大家好,我是王有志。關注王有志,一起聊技術,聊遊戲,從北漂生活談到國際風雲。
最近搞了個抽獎送書的活動,歡迎點選連結參與。
今天開始,我會和大家一起深入學習synchronized
的原理,原理部分會涉及到兩篇:
今天我們先來學習偏向鎖升級到輕量級鎖的過程。因為涉及到大量HotSpot原始碼,會有單獨的一篇註釋版原始碼的文章。
通過本篇文章,你們解答synchronized都問啥?中統計到的如下問題:
正式開始分析synchronized
原始碼前,我們先做一些準備:
Tips:
javap
命令和IDEA自帶的位元組碼工具;準備一個簡單的範例程式碼:
public class SynchronizedPrinciple {
private int count = 0;
private void add() {
synchronized (this) {
count++;
}
}
}
通過工具,我們可以得到如下位元組碼:
aload_0
dup
astore_1
monitorenter // 1
aload_0
dup
getfield #2 <com/wyz/keyword/synckeyword/SynchronizedPrinciple.count : I>
iconst_1
iadd
putfield #2 <com/wyz/keyword/synckeyword/SynchronizedPrinciple.count : I>
aload_1
monitorexit // 2
goto 24 (+8)
astore_2
aload_1
monitorexit // 3
aload_2
athrow
return
synchronized
修飾程式碼塊,編譯成了兩條指令:
我們注意到,monitorexit
出現了兩次。註釋2的部分是程式執行正常,註釋3的部分是程式執行異常。Java團隊連程式異常的情況都替你考慮到了,他真的,我哭死。
Tips:
synchronized
修飾程式碼塊作為範例的原因是,修飾方法時僅在access_flag設定ACC_SYNCHRONIZED
標誌,並不直觀;monitorexit
退出監視器, Java曾在Unsafe
類中提供過進出監視器的方法。Unsafe.getUnsafe.monitorEnter(obj);
Unsafe.getUnsafe.monitorExit(obj);
Java 8可以使用,Java 11已經移除,具體移除的版本我就不太清楚了。
可以通過jol-core來跟蹤物件狀態。
Maven依賴:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
使用範例:
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
在HotSpot中,monitorenter
指令對應這兩大類解析方式:
由於bytecodeInterpreter
基本退出了歷史舞臺,我們以模板直譯器X86實現templateTable_x86為例。
Tips:
monitorenter
的執行方法是templateTable_x86#monitorenter,該方法中,我們只需要關注4438行執行的__ lock_object(rmon)
,呼叫了interp_masm_x86#lock_object方法:
void InterpreterMacroAssembler::lock_object(Register lock_reg) {
if (UseHeavyMonitors) {// 1
// 重量級鎖邏輯
call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter), lock_reg);
} else {
Label done;
Label slow_case;
if (UseBiasedLocking) {// 2
// 偏向鎖邏輯
biased_locking_enter(lock_reg, obj_reg, swap_reg, tmp_reg, false, done, &slow_case);
}
// 3
bind(slow_case);
call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter), lock_reg);
bind(done);
......
}
註釋1和註釋2的部分,是兩個JVM引數:
// 啟用重量級鎖
-XX:+UseHeavyMonitors
// 啟用偏向鎖
-XX:+UseBiasedLocking
註釋1和註釋3,呼叫InterpreterRuntime::monitorenter
方法,註釋1是直接使用重量級鎖的設定,那麼可以猜到,註釋3是獲取偏向鎖失敗鎖升級為重量級鎖的邏輯。
正式開始前,先來了解物件頭(markOop)。實際上,markOop
的註釋已經揭露了它的「祕密「:
The markOop describes the header of an object.
......
Bit-format of an object header (most significant first, big endian layout below):
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)
......
[JavaThread | epoch | age | 1 | 01] lock is biased toward given thread
[0 | epoch | age | 1 | 01] lock is anonymously biase
註釋詳細的描述了64位元大端模式下Java物件頭的結構:
Tips:
markOop
的結構,我沒粘出來~~物件頭中的大部分結構都很容易理解,但epoch是什麼?
註釋中將epoch描述為「used in support of biased locking」。OpenJDK wiki中Synchronization是這樣描述epoch的:
An epoch value in the class acts as a timestamp that indicates the validity of the bias.
epoch類似於時間戳,表示偏向鎖的有效性。它的在批次重偏向階段(biasedLocking#bulk_revoke_or_rebias_at_safepoint)更新:
static BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint(oop o, bool bulk_rebias, bool attempt_rebias_of_object, JavaThread* requesting_thread) {
{
if (bulk_rebias) {
if (klass->prototype_header()->has_bias_pattern()) {
klass->set_prototype_header(klass->prototype_header()->incr_bias_epoch());
}
}
}
}
JVM通過epoch
來判斷是否適合偏向鎖,超過閾值後JVM會升級偏向鎖。JVM提供了引數來調節這個閾值。
// 批次重偏向閾值
-XX:BiasedLockingBulkRebiasThreshold=20
// 批次復原閾值
-XX:BiasedLockingBulkRevokeThreshold=40
Tips:更新的是klass
的epoch
。
系統開啟了偏向鎖,會進入macroAssembler_x86#biased_locking_enter方法。該方法首先是獲取物件的markOop
:
Address mark_addr (obj_reg, oopDesc::mark_offset_in_bytes());
Address saved_mark_addr(lock_reg, 0);
我將接下來的流程分為5個分支,按照執行順序和大家一起分析偏向鎖的實現邏輯。
Tips:
偏向鎖的前置條件,邏輯非常簡單,判斷當前物件markOop的鎖標誌,如果已經升級,執行升級流程;否則繼續向下執行。
Tips:虛線部分邏輯位於其它類中。
目前JVM已知markOop
的鎖標誌位為0B0101
,處於可偏向狀態,但不清楚是已經偏向還是尚未偏向。HotSopt中使用anonymously形容可偏向但尚未偏向某個執行緒的狀態,稱這種狀態為匿名偏向。此時物件頭如下:
此時要做的事情就比較簡單了,判斷是否為當前執行緒重入偏向鎖。如果是重入,直接退出即可;否則繼續向下執行。
Tips:今天刷到一個貼文,Javaer和C++er爭論可重入鎖和遞迴鎖,有興趣的可以看一文看懂並行程式設計中的鎖我簡單解釋了可重入鎖和遞迴鎖的關係。
註釋描述了不是重入偏向鎖的情況:
At this point we know that the header has the bias pattern and that we are not the bias owner in the current epoch. We need to figure out more details about the state of the header in order to know what operations can be legally performed on the object's header.
此時可能存在兩種情況:
偏向鎖復原的部分稍微複雜,使用物件klass
的markOop
替換物件的markOop
,關鍵技術是CAS。
目前偏向鎖的狀態是可偏向,且偏向其他執行緒。此時的邏輯只需要片段epoch
是否有效即可。
重新偏向的可以用一句話描述,構建markOop
進行CAS替換。
目前偏向鎖的狀態是,可偏向,偏向其它執行緒,epoch未過期。此時要做的是在markOop
中設定當前執行緒,也就是偏向鎖重新偏向的過程,和分支4的部分非常相似。
獲取偏向鎖失敗後,執行InterpreterRuntime::monitorenter
方法,位於interpreterRuntime中:
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
if (UseBiasedLocking) {
// 完整的鎖升級路徑
// 偏向鎖->輕量級鎖->重量級鎖
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
// 跳過偏向鎖的鎖升級路徑
// 輕量級鎖->重量級鎖
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
IRT_END
ObjectSynchronizer::fast_enter
位於synchronizer.cpp#fast_enter:
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
// 復原和重偏向
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
BiasedLocking::revoke_at_safepoint(obj);
}
}
// 跳過偏向鎖
slow_enter(obj, lock, THREAD);
}
BiasedLocking::revoke_and_rebias
的精簡註釋版放在了偏向鎖原始碼分析的第2部分。
如果獲取偏向鎖失敗,此時會執行ObjectSynchronizer::slow_enter
,該方法位於synchronizer#slow_enter:
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
// 無鎖狀態 ,獲取偏向鎖失敗後有復原邏輯,此時變為無鎖狀態
if (mark->is_neutral()) {
// 將物件的markOop複製到displaced_header(Displaced Mark Word)上
lock->set_displaced_header(mark);
// CAS將物件markOop中替換為指向鎖記錄的指標
if (mark == obj()->cas_set_mark((markOop) lock, mark)) {
// 替換成功,則獲取輕量級鎖
TEVENT(slow_enter: release stacklock);
return;
}
} else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
// 重入情況
lock->set_displaced_header(NULL);
return;
}
// 重置displaced_header(Displaced Mark Word)
lock->set_displaced_header(markOopDesc::unused_mark());
// 鎖膨脹
ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);
}
直接參照《Java並行程式設計的藝術》中關於輕量級鎖加鎖的過程:
執行緒在執行同步塊之前,JVM會先在當前執行緒的棧楨中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中,官方稱為Displaced Mark Word。然後執行緒嘗試使用CAS將物件頭中的Mark Word替換為指向鎖記錄的指標。如果成功,當前執行緒獲得鎖,如果失敗,表示其他執行緒競爭鎖,當前執行緒便嘗試使用自旋來獲取鎖。
輕量級鎖的邏輯非常簡單,使用到的關鍵技術也是CAS。
此時markOop的結構如下:
處於偏向鎖或者輕量級鎖時,monitorexit
的邏輯非常簡單。有了monitorenter
的經驗,我們很容易分析到monitorexit
的呼叫邏輯:
程式碼就留給大家自行探索了,在這裡給出我的理解。
通常,我會簡單的認為偏向鎖退出時,什麼都不需要做(即偏向鎖不會主動釋放);而對於輕量級鎖來說,至少需要經歷兩個步驟:
因此,從退出邏輯上來說,輕量級鎖的效能是稍遜於偏向鎖的。
我們對這一階段的內容做個簡單的總結,偏向鎖和輕量級鎖的邏輯並不複雜,尤其是輕量級鎖。
偏向鎖和輕量級鎖的關鍵技術都是CAS,當CAS競爭失敗,說明有其它執行緒嘗試搶奪,從而導致鎖升級。
偏向鎖在物件markOop
中記錄第一次持有它的執行緒,當該執行緒不斷持有偏向鎖時,只需要簡單的比對即可,適合絕大部分場景是單執行緒執行,但偶爾可能會存線上程競爭的場景。
但問題是,如果執行緒交替持有執行,偏向鎖的復原和重偏向邏輯複雜,效能差。因此引入了輕量級鎖,用來保證交替進行這種「輕微」競爭情況的安全。
另外,關於偏向鎖的爭議比較多,主要在兩點:
實際上,Java 15中已經放棄了偏向鎖(JEP 374: Deprecate and Disable Biased Locking),但由於大部分應用還跑在Java 8上,我們還是要了解偏向鎖的邏輯。
最後再闢個謠(或者是被打臉?),輕量級鎖中並沒有任何自旋的邏輯。
Tips:好像漏掉了批次復原和批次重偏向~~
好了,今天就到這裡了,Bye~~