synchronized分析

該文章是本人學習記錄總結的,有誤請指出,感謝。java

一開始學習Java時,介紹Java的同步機制那就必然是synchronized。但以後又瞭解到synchronized是一個重量級鎖,因此應當儘可能使用Lock。編程

以後又瞭解到Java1.6對synchronized進行了優化。數組

因此除非:安全

  • 業務須要獲取鎖能夠被中斷
  • 須要獲取鎖能夠超時
  • 能夠嘗試着獲取鎖

的狀況下使用Lock,應當儘可能使用synchronized。代碼更加簡潔。併發

1、synchronized介紹

由於是Java語法提供的,也能夠稱爲內置鎖。ide

根據做爲鎖的對象不一樣,可分爲性能

  • 對象鎖
    • 聲明在非靜態方法上時,以當前類的實例做爲鎖。
    • 同步代碼塊中,以括號裏的對象做爲鎖。
  • 類鎖。
    • 聲明在靜態方法上時則以當前類的字節碼對象做爲鎖也就是類鎖。

2、synchronized實現

從上圖能夠看出synchronized代碼塊經過monitorenter和monitorexit指令實現,由JVM保證monitorenter保證有一個配對的monitorexit。學習

synchronized方法則沒有特別的指令,而是在Class文件的方法表中將該方法的access_flags字段中的synchronized標誌位置1,表示該方法是同步方法並使用調用該方法的對象或該方法所屬的Class在JVM的內部對象表示Klass作爲鎖對象。測試

3、Java對象頭和Monitor

先要了解一下Java對象頭和monitor

一、對象頭

對象頭由三部分組成

1.一、Mark Word

Mark Word爲一個字大小,即在32位JVM長度爲32位,在64位JVM長度爲64位。

由於Mark Word用於存儲與對象自定義數據無關的數據,爲了節省空間,會根據對象的狀態不一樣存放不一樣的數據。

32位JVM存儲格式:

狀態(State) 25bit 4bit 1bit 2bit
23bit 2bit 是否偏向鎖(biased_lock):1bit 鎖標誌位(lock):2bit
無鎖(normal) 對象的散列值(identity_hashcode) 分代年齡(age) 0 01
偏向鎖(Biased) 線程ID(threadID) 偏向時間戳(epoch) 分代年齡(age) 1 01
輕量級鎖(Lightweight Locked) 指向棧中記錄的指針(ptr_to_lock_record) 00
重量級鎖(Heavyweight Locked) 指向管程的指針(ptr_to_heavyweight_monitor) 10
GC標記(Marked for GC) 空(null) 11

JDK1.6以後存在鎖升級的概念,JVM對同步鎖的處理隨着競爭激烈,處理方式從偏向鎖到輕量級鎖再到重量級鎖。

1.二、klass pointer

用於存儲對象的類型指針,該指針指向它的元數據,大小爲一個字。

1.三、array length(數組對象纔有)

只有數組對象纔有這部分數據

由於JVM虛擬機能夠經過Java對象的元數據信息肯定Java對象的大小,可是沒法從數組的元數據來確認數組的大小,因此用一塊來記錄數組長度。

參考:

Java對象頭詳解

Java的對象頭和對象組成詳解

二、Monitor Record

Monitor Record是線程私有的,每一個線程都有一個Monitor Record列表,同時還有一個全局可用列表。每個做爲鎖的對象都會與一個Monitor Record關聯(對象頭的MarkWord中的LockWord指向monitor record的起始地址)。

Owner
EntryQ
RcThis
Nest
HashCode
Candidate

Owner:初始時爲NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖後保存線程惟一標識,當鎖被釋放時又設置爲NULL;

EntryQ:關聯一個系統互斥鎖(semaphore),阻塞全部試圖鎖住monitor record失敗的線程。

RcThis:表示blocked或waiting在該monitor record上的全部線程的個數。

Nest:用來實現重入鎖的計數。

HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。

Candidate:用來避免沒必要要的阻塞或等待線程喚醒,由於每一次只有一個線程可以成功擁有鎖,若是每次前一個釋放鎖的線程喚醒全部正在阻塞或等待的線程,會引發沒必要要的上下文切換(從阻塞到就緒而後由於競爭鎖失敗又被阻塞)從而致使性能嚴重降低。Candidate只有兩種可能的值0表示沒有須要喚醒的線程,1表示要喚醒一個繼任線程來競爭鎖。

4、synchronized的優化

鎖粗化(Lock Coarsening)

也就是減小沒必要要的緊連在一塊兒的unlock,lock操做,將多個連續的鎖擴展成一個範圍更大的鎖。

鎖消除(Lock Elimination)

經過運行時JIT編譯器的逃逸分析來消除一些沒有在當前同步塊之外被其餘線程共享的數據的鎖保護,經過逃逸分析也能夠在線程本地Stack上進行對象空間的分配(同時還能夠減小Heap上的垃圾收集開銷)。

適應性自旋(Adaptive Spinning)

所謂自適應就意味着自旋的次數再也不是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。線程若是自旋成功了,那麼下次自旋的次數會更加多,由於虛擬機認爲既然上次成功了,那麼這次自旋也頗有可能會再次成功,那麼它就會容許自旋等待持續的次數更多。反之,若是對於某個鎖,不多有自旋可以成功的,那麼在之後要或者這個鎖的時候自旋的次數會減小甚至省略掉自旋過程,以避免浪費處理器資源。

偏向鎖(Biased Locking)和輕量級鎖(Lightweight Locking)

一開始我對於鎖的瞭解就是拿到了就執行任務,拿不到就阻塞。

Java的線程時是映射到操做系統原生線程上的,線程的阻塞和喚醒都須要操做系統的介入,須要在用戶態和核心態之間轉換當,這種切換會消耗掉大量的系統資源(由於用戶態和系統態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態須要傳遞給許多變量、參數給內核,內核也須要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束後切換回用戶態繼續工做 摘自:Java線程阻塞的代價)。

所以,JVM使用鎖會逐步升級:無鎖->偏向鎖->輕量級鎖->重量級鎖

鎖只能升級,不能降級

  1. ​ 初始沒有線程使用鎖,Mark Word爲無鎖狀態

  2. 偏向鎖:
    加鎖
    • 測試Mark Word中是否指向當前線程,是的話表示當前線程已獲取該鎖,不是則判斷是不是偏向鎖。
    • 不是偏向鎖則會CAS操做在對象頭存儲當前線程ID,之後該線程在進入和退出同步塊時不須要花費CAS操做來加鎖和解鎖,而只需簡單的測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖。
    • 如果偏向鎖則表示當前鎖有其它線程在使用,存在競爭,偏向鎖會膨脹爲輕量級鎖。
    膨脹
    • 當前線程CAS獲取鎖失敗,撤銷偏向鎖(不是釋放的意思,偏向鎖沒有釋放):發現鎖存在競爭,

    等待全局安全點(此時間點,全部的工做線程都停了字節碼的執行),經過ID找到已得到偏向鎖的線程,掛起該線程,從該線程的Monitor Record列表得到一個空閒記錄,並將鎖對象的對象頭設爲輕量級鎖狀態,將Lock Record更新爲指向該空閒記錄的指針。到這裏鎖撤銷完成,被掛起的線程繼續運行。

    • 撤銷完成後,對象可能處於兩種狀態,不可偏向的無鎖狀態和不可偏向的已鎖狀態。
      • 不可偏向的無鎖狀態:本來獲取偏向鎖的線程執行完了同步塊。
      • 不可偏向的已鎖狀態:本來獲取偏向鎖的線程未執行完了同步塊。此時對象應該被轉換爲輕量級加鎖的狀態。
    批量再偏向:

    ​ 偏向鎖這個機制很特殊, 別的鎖在執行完同步代碼塊後, 都會有釋放鎖的操做, 而偏向鎖並無直觀意義上的「釋放鎖」操做。

    那麼做爲開發人員, 很天然會產生的一個問題就是, 若是一個對象先偏向於某個線程, 執行完同步代碼後, 另外一個線程就不能直接從新得到偏向鎖嗎? 答案是能夠, JVM 提供了批量再偏向機制(Bulk Rebias)機制

    該機制的主要工做原理以下:

    • 引入一個概念 epoch, 其本質是一個時間戳 , 表明了偏向鎖的有效性
    • 從前文描述的對象頭結構中能夠看到, epoch 存儲在可偏向對象的 MarkWord 中。
    • 除了對象中的 epoch, 對象所屬的類 class 信息中, 也會保存一個 epoch 值
    • 每當遇到一個全局安全點時, 若是要對 class C 進行批量再偏向, 則首先對 class C 中保存的 epoch 進行增長操做, 獲得一個新的 epoch_new
    • 而後掃描全部持有 class C 實例的線程棧, 根據線程棧的信息判斷出該線程是否鎖定了該對象, 僅將 epoch_new 的值賦給被鎖定的對象中。
    • 退出安全點後, 當有線程須要嘗試獲取偏向鎖時, 直接檢查 class C 中存儲的 epoch 值是否與目標對象中存儲的 epoch 值相等, 若是不相等, 則說明該對象的偏向鎖已經無效了, 能夠嘗試對此對象從新進行偏向操做。
  3. 輕量級鎖:
    加鎖:
    1. 經過對象判斷鎖對象對象頭是不是無鎖狀態,是則JVM首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲所對象的Mark Word的拷貝(官方把這份拷貝加了一個Displaced前綴,即Displaced Mark Word),拷貝成功後,嘗試CAS操做將Mark Word的Lock Record更新爲指向moniter record的指針。若更新失敗,則表示競爭很激烈,須要膨脹爲重量級鎖。
    2. 鎖對象處於不可偏向無鎖狀態,多個線程CAS操做試圖將Mark Word更新爲指向本身的Monitor Word的指針,更新成功的獲取到鎖,失敗的線程進入狀況4。
    3. 鎖對象處於不可偏向的已鎖狀態,同時Mark Word中是指向本身的Monitor Word,這就是重入(reentrant)鎖的狀況,只須要簡單的將Nest加1便可。不須要任何原子操做,效率很是高。
    4. 鎖對象處於不可偏向的已鎖狀態,同時Mark Word中不是指向本身的Monitor Word,線程自旋必定次數仍然獲取失敗後膨脹。
    釋放鎖:
    1. 首先檢查該對象是否處於膨脹狀態而且該線程是這個鎖的擁有者,若是發現不對則拋出異常;
    2. 檢查Nest字段是否大於1,若是大於1則簡單的將Nest減1並繼續擁有鎖,若是等於1,則進入到第3步;
    3. 檢查rfThis是否大於0,設置Owner爲NULL而後喚醒一個正在阻塞或等待的線程再一次試圖獲取鎖,若是等於0則進入到第4步
    4. 縮小(deflate)一個對象,經過將對象的LockWord置換回原來的HashCode值來解除和monitor record之間的關聯來釋放鎖,同時將monitor record放回到線程是有的可用monitor record列表。
  4. 重量級鎖:

    重量級鎖依賴於操做系統的互斥量(mutex) 實現。

  5. 小結

    偏向鎖、輕量級鎖、重量級鎖適用於不一樣的併發場景:

    • 偏向鎖:無實際競爭,且未來只有第一個申請鎖的線程會使用鎖。
    • 輕量級鎖:無實際競爭,多個線程交替使用鎖;容許短期的鎖競爭。
    • 重量級鎖:有實際競爭,且鎖競爭時間長。

    另外,若是鎖競爭時間短,可使用自旋鎖進一步優化輕量級鎖、重量級鎖的性能,減小線程切換。
    若是鎖競爭程度逐漸提升(緩慢),那麼從偏向鎖逐步膨脹到重量鎖,可以提升系統的總體性能。

參考:

Java中的偏向鎖,輕量級鎖, 重量級鎖解析

Java中synchronized的實現原理與應用

Java的對象頭和對象組成詳解

【java併發編程實戰4】偏向鎖-輕量鎖-重量鎖的那點祕密(synchronize實現原理)

整篇參考:

JVM內部細節之一:synchronized關鍵字及實現細節(輕量級鎖Lightweight Locking)

【死磕Java併發】—–深刻分析synchronized的實現原理

相關文章
相關標籤/搜索