一晚上搞懂 | JVM 線程安全與鎖優化

前言

本文已經收錄到個人 Github 我的博客,歡迎大佬們光臨寒舍:html

個人 GIthub 博客git

學習導圖

學習導圖

一.爲何要學習內存模型與線程?

以前咱們學習了內存模型和線程,瞭解了 JMM 和線程,初步探究了 JVM 怎麼實現併發,而本篇文章,咱們的關注點是 JVM 如何實現高效程序員

併發編程的目的是爲了讓程序運行得更快,提升程序的響應速度,雖然咱們但願經過多線程執行任務讓程序運行得更快,可是同時也會面臨很是多的挑戰,好比像線程安全問題、線程上下文切換的問題、硬件和軟件資源限制等問題,這些都是併發編程給咱們帶來的難題。github

其中線程安全問題是咱們最關心的問題之一,咱們接下來主要就圍繞着線程安全的問題來展開。編程

二.核心知識點概括

2.1 線程安全

2.1.1 定義

當多個線程訪問一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替執行,也不須要進行額外的同步,或者在調用方進行任何其餘的協調操做,調用這個對象的行爲均可以得到正確的結果,那這個對象是線程安全的數組

要求線程安全的代碼都必須具有一個特徵: 代碼自己封裝了全部必要的正確性保障手段(如互斥同步等),令調用者無須關心多線程的問題,更無須本身採起任何措施來保證多線程的正確調用。安全

2.1.2 分類

下面將按照線程安全的程度由強至弱分紅五類多線程

  • 不可變:外部的可見狀態永遠不會改變,在多個線程之中永遠是一致的狀態
  • 必定是線程安全的併發

  • 如何實現函數

    1.若是共享數據是一個基本數據類型,只要在定義時用 final 關鍵字修飾

    2.若是共享數據是一個對象,最簡單的方法是把對象中帶有狀態的變量都聲明爲 final(例如 String 類的實現)

  • 絕對線程安全:徹底知足以前給出的線程安全的定義,即達到『無論運行時環境如何,調用者都不須要任何額外的同步措施』
  • 相對線程安全:能保證對該對象單獨的操做是線程安全的,在調用時無需作額外保障措施,但對於一些特定順序的連續調用,可能須要在調用端使用額外的同步措施來保證調用的正確性
  • 一般意義上所講的線程安全
  • 大部分的線程安全類都屬於這種類型,如 VectorHashTableCollections#synchronizedCollection() 包裝的集合等
  • 線程兼容:對象自己非線程安全的,但能夠經過在調用端正確地使用同步手段來保證對象在併發環境中能夠安全地使用
  • 是一般意義上所講的非線程安全
  • Java API 中大部分類都是屬於線程兼容的,如 ArrayListHashMap
  • 線程對立:不管調用端是否採起了同步措施,都沒法在多線程環境中併發使用的代碼

例子:Thread 類的 suspend()resume() ,一個嘗試中斷線程,一個嘗試恢復線程,在併發條件下,有可能會形成死鎖

2.1.3 實現

可分紅兩大手段:

  • 經過代碼編寫實現線程安全
  • 經過虛擬機自己實現同步與鎖

本篇重點在虛擬機自己

1.互斥同步
  • 含義:
  • 同步:在多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一個線程使用

  • 互斥:是實現同步的一種手段,臨界區、互斥量和信號量都是主要的互斥實現方式

  • 二者關係:互斥是因,同步是果;互斥是方法,同步是目的

  • 屬於悲觀併發策略悲觀鎖),即認爲只要不作正確的同步措施就確定會出現問題,所以不管共享數據是否真的會出現競爭,都要加鎖

  • 最大的問題是進行線程阻塞和喚醒所帶來的性能問題,也稱爲阻塞同步

  • 使用方式:

    A.使用 synchronized 關鍵字:

  • 原理:編譯後會在同步塊的先後分別造成 monitorentermonitorexit 這兩個字節碼指令,並經過一個 reference 類型的參數來指明要鎖定和解鎖的對象

    注意:

    ​ 1.若明確指定了對象參數,則取該對象的 reference

    ​ 2.不然,會根據 synchronized 修飾的是實例方法仍是類方法去取對應的對象實例或 Class 對象來做爲鎖對象

    synchronized 處理邏輯

  • 過程:執行 monitorenter 指令時先要嘗試獲取對象的鎖。若該對象沒被鎖定或者已被當前線程獲取,那麼鎖計數器 + 1;而在執行 monitorexit 指令時,鎖計數器 - 1;當鎖計數器 = 0 時,鎖就被釋放;若獲取對象鎖失敗,那當前線程會一直被阻塞等待,直到對象鎖被另一個線程釋放爲止

  • 特別注意

    1.synchronized 同步塊對同一條線程來講是可重入的,不會出現自我鎖死的問題

    2.同步塊在已進入的線程執行完以前,會阻塞後面其餘線程的進入

​ B.使用重入鎖 ReentrantLock

​ 以前在 進階之路 | 奇妙的 Thread 之旅中也提到太重入鎖的使用,相信看過的讀者還有一些印象

  • synchronized 的相同:用法與 synchronized 很類似,且均可重入

  • synchronized不一樣

    1.等待可中斷:當持有鎖的線程長期不釋放鎖的時候,正在等待的線程能夠選擇放棄等待,改成處理其餘事情

    2.公平鎖:多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次得到鎖。而 synchronized 是非公平的,即在鎖被釋放時,任何一個等待鎖的線程都有機會得到鎖。ReentrantLock 默認狀況下也是非公平的,但能夠經過帶布爾值的構造函數改用公平鎖

    3.鎖綁定多個條件:一個 ReentrantLock 對象能夠經過屢次調用 newCondition() 同時綁定多個 Condition 對象。而在 synchronized 中,鎖對象的 wait()notify()notifyAl() 只能實現一個隱含的條件,若要和多於一個的條件關聯不得不額外地添加一個鎖

  • 選擇:在 synchronized 能實現需求的狀況下,優先考慮使用它來進行同步。理由以下:
  • synchronizedJava語法層面的同步,足夠清晰簡單
  • Lock 必須由程序員確保在 finally 塊中釋放鎖,而 synchronized 能夠由 JVM 確保鎖的自動釋放
2.非阻塞同步
  • 定義:基於衝突檢測的樂觀併發策略(樂觀鎖),即先進行操做,若無其餘線程爭用共享數據,操做成功;反之產生了衝突再去採起其餘的補償措施
  • 爲了保證操做衝突檢測這兩步具有原子性,須要用到硬件指令集,好比:
  • 測試並設置
  • 獲取並增長
  • 交換
  • 比較並交換CAS
  • 加載連接 / 條件存儲
3.無同步方案
  • 定義:不用同步的方式保證線程安全,由於有些代碼天生就是線程安全的。
  • 例子:

A.可重入代碼/ 純代碼

  • 含義:可在代碼執行的任什麼時候刻中斷它去執行另一段代碼,當控制權返回後原來的程序並不會出現任何錯誤
  • 共同特徵:不依賴存儲在堆上的數據和公用的系統資源、用到的狀態量都由參數中傳入、不調用非可重入的方法
  • 斷定依據:若是一個方法,它的返回結果是可預測的,只要輸入相同的數據就都能返回相同的結果,就知足可重入性
  • 注意:知足可重入性的代碼必定是線程安全的,反之,知足線程安全的代碼不必定是可重入的

B.線程本地存儲

  • 含義:把共享數據的可見範圍限制在同一個線程以內,無須同步就能保證線程之間不出現數據爭用的問題
  • 想詳細瞭解 ThreadLocal 的讀者,能夠看下筆者以前寫的一篇文章:進階之路 | 奇妙的 Handler 之旅

2.2 鎖優化

一圖帶你看遍鎖

解決併發的正確性以後,爲了能在線程之間更『高效』地共享數據、解決競爭問題、提升程序的執行效率,下面介紹五種鎖優化技術

2.2.1 適應性自旋

  • 背景:互斥同步在實現阻塞和喚醒時須要掛起線程和恢復線程的操做,都須要轉入內核態中完成,很影響系統的併發性能;同時,在許多應用上共享數據的鎖定狀態只是暫時,不必去掛起和恢復線程
  • 自旋鎖:當物理機器有多個處理器使得多個線程同時並行執行時,先讓後請求鎖的線程等待,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖,這時只需讓線程執行一個忙循環,即自旋

注意:自旋等待不能代替阻塞,它雖然能避免線程切換的開銷,但會佔用處理器時間,所以自旋等待的時間必需要有必定的限度,若是自旋超過了限定的次數(默認10次)仍未成功獲鎖,就須要掛線程了

  • 自適應自旋鎖:自旋的時間再也不固定,而是由該鎖上的上次自旋時間及鎖的擁有者的狀態共同決定。具體表現是:
  • 若是對於某個鎖,自旋等待剛剛成功得到,且持有鎖的線程正在運行中,那麼虛擬機極可能容許自旋等待的時間更久點
  • 若是對於某個鎖,自旋不多成功得到過,那麼極可能之後將省略自旋等待這個鎖,避免浪費處理器資源

2.2.2 鎖消除

  • 定義:指虛擬機即時編譯器在運行時,對一些代碼上要求同步,可是被檢測到不可能存在共享數據競爭的鎖進行消除
  • 斷定依據:若是一段代碼中上的全部數據都不會逃逸出去被其餘線程訪問到,可把它們當作上數據對待,即線程私有的,無須同步加鎖

2.2.3 鎖粗化

  • 通常狀況下,會將同步塊的做用範圍限制到只在共享數據的實際做用域中才進行同步,使得須要同步的操做數量儘量變小,保證就算存在鎖競爭,等待鎖的線程也能儘快拿到鎖
  • 但若是反覆操做對同一個對象進行加鎖和解鎖,即便沒有線程競爭,頻繁地進行互斥同步操做也會致使沒必要要的性能損耗,此時,虛擬機將會把加鎖同步的範圍粗化到整個操做序列的外部,這樣只需加一次鎖

2.2.4 輕量級鎖

  • 目的:在沒有多線程競爭的前提下減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗,注意不是用來代替重量級鎖的

首先先理解 HotSpot 虛擬機的對象頭的內存佈局:分爲兩部分

  • 第一部分用於存儲對象自身的運行時數據,這部分被稱爲 Mark Word,是實現輕量級鎖和偏向鎖的關鍵。如哈希碼、GC 分代年齡等
  • 另一部分用於存儲指向方法區對象類型數據的指針,若是是數組對象還會有一個額外的部分用於存儲數組長度

Mark Word 結構

  • 加鎖過程:

    1.代碼進入同步塊時,若是同步對象未被鎖定(鎖標誌位爲 01),虛擬機會在當前線程的棧幀中創建一個名爲 Lock Record 的空間,用於存儲鎖對象 Mark Word 的拷貝。以下圖

2.以後虛擬機會嘗試用 CAS 操做將對象的 Mark Word 更新爲指向 Lock Record 的指針。若更新動做成功,那麼當前線程就擁有了該對象的鎖,且對象 Mark Word 的鎖標誌位變爲 00,即處於輕量級鎖定狀態;反之,虛擬機會先檢查對象的 Mark Word 是否指向當前線程的棧幀,如果,則當前線程已有該對象的鎖,可直接進入同步塊繼續執行,不然說明改對象已被其餘線程搶佔。以下圖:

CAS後堆棧與對象的狀態

另外,若是有兩條以上的線程爭用同一個鎖,那輕量級鎖就再也不有效,要膨脹爲重量級鎖,鎖標誌位變爲 10Mark Word 中存儲的就是指向重量級鎖的指針,後面等待鎖的線程也要進入阻塞狀態

加鎖流程

  • 解鎖過程:若對象的 Mark Word 仍指向着線程的 Lock Record,就用 CAS 操做把對象當前的 Mark Word 和線程中複製的 Displaced Mark Word 替換回來。若替換成功,那麼就完成了整個同步過程;反之,說明有其餘線程嘗試獲取該鎖,那麼就要在釋放鎖的同時喚醒被掛起的線程

    解鎖過程

  • 優勢:由於對於絕大部分的鎖,在整個同步週期內都是不存在競爭的,因此輕量級鎖經過使用 CAS 操做消除同步使用的互斥量

  • 自旋鎖和輕量級鎖的關係:

  • 自旋鎖是爲了減小線程掛起次數
  • 輕量級鎖是在加鎖的時候,如何使用一種更高效的方式來加鎖

Q:處於輕量級鎖狀態時,會不會使用自旋鎖這個競爭機制

A:線程首先會經過 CAS 獲取鎖,失敗後經過自旋鎖來嘗試獲取鎖,再失敗鎖就膨脹爲重量級鎖。因此輕量級鎖狀態下可能會有自旋鎖的參與(CAS 將對象頭的標記指向鎖記錄指針失敗的時候)

2.2.5 偏向鎖

  • 目的:消除數據在無競爭狀況下的同步原語,進一步提升程序的運行性能
  • 若是說輕量級鎖是在無競爭的狀況下使用 CAS消除同步使用的互斥量

  • 偏向鎖就是在無競爭狀況下把整個同步都消除掉

  • 含義:偏向鎖會偏向於第一個得到它的線程,若是在後面的執行中該鎖沒有被其餘的線程獲取,則持有偏向鎖的線程將永遠不須要再進行同步

  • 加鎖過程:啓用偏向鎖的鎖對象在第一次被線程獲取時,Mark Word 的鎖標誌位會被設置爲 01,即偏向模式,同時使用 CAS 操做把獲取到這個鎖的線程 ID 記錄在對象的 Mark Word 中。若操做成功,持有偏向鎖的線程之後每次進入這個鎖相關的同步塊時均可再也不進行任何同步操做

  • 解鎖過程:當有另外的線程去嘗試獲取這個鎖時,根據鎖對象目前是否處於被鎖定的狀態,撤銷偏向後恢復到未鎖定 01 或輕量級鎖定 00 的狀態,後續的同步操做就如輕量級鎖執行過程。以下圖:

  • 優勢:可提升帶有同步但無競爭的程序性能,但若程序中大多數鎖總被多個線程訪問,此模式就不必了

解放啦

三.碎碎念

可以寫出高性能、高伸縮性的併發程序是一門藝術,而瞭解併發在底層是如何實現的,則是掌握這門藝術的前提,也是成長爲高級程序員的必備知識!

加油吧!騷年!以夢爲馬,不負韶華!

衝鴨


若是文章對您有一點幫助的話,但願您能點一下贊,您的點贊,是我前進的動力

本文參考連接:

相關文章
相關標籤/搜索