bilibili-Java併發學習筆記5 鎖升級(偏向鎖/輕量級鎖/重量級鎖)、鎖消除、鎖粗化
基於 java 1.8.0
P18_鎖升級與偏向鎖深入解析
- 在 JDK 1.5 之前(不包括1.5),若想實現線程同步,只能使用 synchronized 關鍵字這一方式來達到;jdk 層面,也是通過 synchronized 關鍵字來做到數據的原子性維護的;synchronized 關鍵字時 JVM 實現的一種內置鎖,從 jvm 層面角度看,這種鎖的獲取和釋放都是由 JVM 來完成的;
- 在 JDK 1.5 中,引入 JUC 併發包,其中包含很多併發工具和鎖,Lock 同步鎖是基於 Java 來實現的,因此鎖的獲取與釋放都是通過 Java 代碼來實現與控制的;而 synchronized 是基於底層操作系統的
Mutex Lock
來實現的,每次對鎖的獲取與釋放都會帶來用戶態和內核態之間的切換,這種切換會極大地增加系統負擔;在併發量較高時,也就是鎖的競爭比較激烈時
,synchronized 鎖在性能上的表現會較差。 - 從 JDK 1.6 開始,synchronized 鎖的實現發生了很大的變化;JVM 引入了相應的優化手段來提升 synchronized 鎖的性能,這種提升涉及到
偏向鎖
、輕量級鎖
、重量級鎖
等,從而減少鎖的競爭所帶來的用戶態和內核態之間的切換;這種鎖的優化是通過Java 對象頭
中的一些標誌位來實現的; - 從 JDK 1.6 開始,對象實例在堆內存中會由三部分組成:
- 參考數據 JVM學習筆記 番外3 java object header
- 參考數據 JVM學習筆記 番外4 synchronized 鎖狀態
- 對象頭
- Mark Word
- 鎖標記
- GC 標記
- 等等
- 指向類的指針
- 數組長度
- Mark Word
- 實例數據
- 對齊填充(可選)
hotspot/src/share/vm/oops/markOop.hpp
// The markOop describes the header of an object.
//
// Note that the mark is not a real oop but just a word.
// It is placed in the oop hierarchy for historical reasons.
//
// Bit-format of an object header (most significant first, big endian layout below):
//
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
//
// - hash contains the identity hash value: largest value is
// 31 bits, see os::random(). Also, 64-bit vm's require
// a hash value no bigger than 32 bits because they will not
// properly generate a mask larger than that: see library_call.cpp
// and c1_CodePatterns_sparc.cpp.
//
// - the biased lock pattern is used to bias a lock toward a given
// thread. When this pattern is set in the low three bits, the lock
// is either biased toward a given thread or "anonymously" biased,
// indicating that it is possible for it to be biased. When the
// lock is biased toward a given thread, locking and unlocking can
// be performed by that thread without using atomic operations.
// When a lock's bias is revoked, it reverts back to the normal
// locking scheme described below.
//
// Note that we are overloading the meaning of the "unlocked" state
// of the header. Because we steal a bit from the age we can
// guarantee that the bias pattern will never be seen for a truly
// unlocked object.
//
// Note also that the biased state contains the age bits normally
// contained in the object header. Large increases in scavenge
// times were seen when these bits were absent and an arbitrary age
// assigned to all biased objects, because they tended to consume a
// significant fraction of the eden semispaces and were not
// promoted promptly, causing an increase in the amount of copying
// performed. The runtime system aligns all JavaThread* pointers to
// a very large value (currently 128 bytes (32bVM) or 256 bytes (64bVM))
// to make room for the age bits & the epoch bits (used in support of
// biased locking), and for the CMS "freeness" bit in the 64bVM (+COOPs).
//
// [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread
// [0 | epoch | age | 1 | 01] lock is anonymously biased
//
// - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
// [ptr | 00] locked ptr points to real header on stack
// [header | 0 | 01] unlocked regular object header
// [ptr | 10] monitor inflated lock (header is wapped out)
// [ptr | 11] marked used by markSweep to mark an object
// not valid at any other time
//
// We assume that stack/thread pointers have the lowest two bits cleared.
P19_輕量級鎖與重量級鎖的變化深入詳解
對於鎖的演化來說,可能經歷如下階段:
無鎖 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖
- 偏向鎖
針對於一個線程來說,它的主要作用就是優化同一個線程多次獲取同一個鎖的情況;如果一個 synchronized 方法被一個線程訪問,那麼這個方法所在的對象實例就會在其 Mark Word 中使用偏向鎖進行標記且存儲該線程的ID;當這個線程再次訪問同一個對象的同步代碼時,它會檢查這個對象的 Mark Word 的鎖標記(若是偏向鎖且線程ID還是自己,那麼該線程無需再去進入管程[Monitor]了,而是直接進入到同步代碼中)。
如果有其他線程訪問此對象的同步代碼時,偏向鎖會被取消;
- 輕量級鎖
若第一個線程已經獲取到了當前對象的鎖,這時第二個線程又開始嘗試爭搶該對象的鎖,由於該對象的鎖已經被第一個線程獲取到,此時還是偏性鎖;而第二個線程在爭搶時,會發現該對象頭中的 Mark Word 的鎖標記是偏向鎖且線程ID不是自己(是第一個線程ID),那麼它會以 CAS(Compare and Swap) 操作的方式去請求鎖:
- 請求鎖成功:將 Mark Word 中的線程ID指向自己,鎖標記不變(還是偏向鎖)
- 請求鎖失敗:表名可能會有多個線程同時在嘗試爭搶該對象的鎖,這時偏向鎖會升級爲輕量級鎖
- 此時這個線程會先進行一段時間的自旋(自旋鎖),等待第一個線程執行完成;
- 自旋很短一段時間後,重新獲取到了鎖,鎖標記???
- 自旋一段時間後,依然無法獲取到鎖(第一個線程在同步代碼中執行時間較長),鎖會繼續升級,升級升重量級鎖
- 在這種情況下,無法獲取到鎖的線程都會進入到 Wait Set (Monitor 內核態)
- 自旋鎖的特點就是
避免線程從用戶態進入到內核態
- 此時這個線程會先進行一段時間的自旋(自旋鎖),等待第一個線程執行完成;
- 重量級鎖
線程最終從用戶態進入到內核態
P20_鎖粗化與鎖消除技術實例演示與分析
package new_package.thread.p20;
public class MyTest {
int i = 0;
public void method() {
Object object = new Object();
synchronized (object) {
i++;
}
System.out.println(i);
}
}
// 其實這個鎖是無用的,因爲每次進入方法都會重新生成一個新的 object 對象;
鎖消除
JIT 編譯器(Just In Time)
可以在動態編譯同步代碼時,使用一種叫做逃逸分析的技術,來通過該技術判別程序中所使用的鎖對象是否只被一個線程所使用而沒有散步到其他線程中;如果是,那麼 JIT 編譯器在編譯這個同步代碼時就不會生成 synchronized 所標識的鎖的申請和釋放的機器碼,從而消除可鎖的使用流程。
這也是編譯器對於鎖的優化措施之一
並不是在字節碼層面進行的優化
package new_package.thread.p20;
public class MyTest2 {
Object object = new Object();
public void method() {
synchronized (object) {
System.out.println("hello");
}
synchronized (object) {
System.out.println("hello2");
}
synchronized (object) {
System.out.println("hello3");
}
}
}
// 本來可以放到一起,但我並沒有放到一起
鎖粗化
JIT 編譯器
在執行動態編譯時,若發現前後相鄰的 synchronized 塊使用的是同一個鎖對象,那麼它就會把這幾個 synchronized 塊合併爲一個較大的同步代碼塊,這樣處理的好處在於線程在執行這些代碼時就無須頻繁申請和釋放鎖,而只需要申請和釋放一次,從而提升了性能。
P21_鎖與底層內容階段性回顧與總結
階段性總結:
Java併發學習筆記1 Thread 類
Java併發學習筆記2 wait 和 notify
Java併發學習筆記3 synchronized
P22_openjdk源碼剖析與鎖升級技術回顧
P23_死鎖檢測與相關工具詳解
JVM學習筆記20 jvisualvm 線程死鎖檢測與分析工具深度解析 65
- 死鎖:線程1等待線程2互斥持有的資源,而線程2也在等待線程1互斥持有的資源,兩個線程都無法繼續執行;
- 活鎖:線程持續重試一個總是失敗的操作,導致無法繼續執行;
- 餓死:線程一直被調度器延遲訪問其依賴的資源,也許是調度器先於低優先級的線程而執行高優先級的線程;同時總是會有一個高優先級的線程可以執行,餓死也叫做無限延遲。