該文章是本人學習記錄總結的,有誤請指出,感謝。java
一開始學習Java時,介紹Java的同步機制那就必然是synchronized。但以後又瞭解到synchronized是一個重量級鎖,因此應當儘可能使用Lock。編程
以後又瞭解到Java1.6對synchronized進行了優化。數組
因此除非:安全
的狀況下使用Lock,應當儘可能使用synchronized。代碼更加簡潔。併發
由於是Java語法提供的,也能夠稱爲內置鎖。ide
根據做爲鎖的對象不一樣,可分爲性能
從上圖能夠看出synchronized代碼塊經過monitorenter和monitorexit指令實現,由JVM保證monitorenter保證有一個配對的monitorexit。學習
synchronized方法則沒有特別的指令,而是在Class文件的方法表中將該方法的access_flags字段中的synchronized標誌位置1,表示該方法是同步方法並使用調用該方法的對象或該方法所屬的Class在JVM的內部對象表示Klass作爲鎖對象。測試
先要了解一下Java對象頭和monitor
對象頭由三部分組成
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對同步鎖的處理隨着競爭激烈,處理方式從偏向鎖到輕量級鎖再到重量級鎖。
用於存儲對象的類型指針,該指針指向它的元數據,大小爲一個字。
只有數組對象纔有這部分數據
由於JVM虛擬機能夠經過Java對象的元數據信息肯定Java對象的大小,可是沒法從數組的元數據來確認數組的大小,因此用一塊來記錄數組長度。
參考:
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表示要喚醒一個繼任線程來競爭鎖。
也就是減小沒必要要的緊連在一塊兒的unlock,lock操做,將多個連續的鎖擴展成一個範圍更大的鎖。
經過運行時JIT編譯器的逃逸分析來消除一些沒有在當前同步塊之外被其餘線程共享的數據的鎖保護,經過逃逸分析也能夠在線程本地Stack上進行對象空間的分配(同時還能夠減小Heap上的垃圾收集開銷)。
所謂自適應就意味着自旋的次數再也不是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。線程若是自旋成功了,那麼下次自旋的次數會更加多,由於虛擬機認爲既然上次成功了,那麼這次自旋也頗有可能會再次成功,那麼它就會容許自旋等待持續的次數更多。反之,若是對於某個鎖,不多有自旋可以成功的,那麼在之後要或者這個鎖的時候自旋的次數會減小甚至省略掉自旋過程,以避免浪費處理器資源。
一開始我對於鎖的瞭解就是拿到了就執行任務,拿不到就阻塞。
Java的線程時是映射到操做系統原生線程上的,線程的阻塞和喚醒都須要操做系統的介入,須要在用戶態和核心態之間轉換當,這種切換會消耗掉大量的系統資源(由於用戶態和系統態都有各自專用的內存空間,專用的寄存器等,用戶態切換至內核態須要傳遞給許多變量、參數給內核,內核也須要保護好用戶態在切換時的一些寄存器值、變量等,以便內核態調用結束後切換回用戶態繼續工做 摘自:Java線程阻塞的代價)。
所以,JVM使用鎖會逐步升級:無鎖->偏向鎖->輕量級鎖->重量級鎖
鎖只能升級,不能降級
初始沒有線程使用鎖,Mark Word爲無鎖狀態
等待全局安全點(此時間點,全部的工做線程都停了字節碼的執行),經過ID找到已得到偏向鎖的線程,掛起該線程,從該線程的Monitor Record列表得到一個空閒記錄,並將鎖對象的對象頭設爲輕量級鎖狀態,將Lock Record更新爲指向該空閒記錄的指針。到這裏鎖撤銷完成,被掛起的線程繼續運行。
偏向鎖這個機制很特殊, 別的鎖在執行完同步代碼塊後, 都會有釋放鎖的操做, 而偏向鎖並無直觀意義上的「釋放鎖」操做。
那麼做爲開發人員, 很天然會產生的一個問題就是, 若是一個對象先偏向於某個線程, 執行完同步代碼後, 另外一個線程就不能直接從新得到偏向鎖嗎? 答案是能夠, JVM 提供了批量再偏向機制(Bulk Rebias)機制
該機制的主要工做原理以下:
重量級鎖依賴於操做系統的互斥量(mutex) 實現。
偏向鎖、輕量級鎖、重量級鎖適用於不一樣的併發場景:
另外,若是鎖競爭時間短,可使用自旋鎖進一步優化輕量級鎖、重量級鎖的性能,減小線程切換。
若是鎖競爭程度逐漸提升(緩慢),那麼從偏向鎖逐步膨脹到重量鎖,可以提升系統的總體性能。
參考:
【java併發編程實戰4】偏向鎖-輕量鎖-重量鎖的那點祕密(synchronize實現原理)
整篇參考: