什么是重量級鎖?
重量級鎖是一種同步機制,通常與在多線程環境中使用synchronized關鍵字實現同步相關。
由于其實現的開銷和復雜性較高,因此被稱為“重量級”,適合需要更嚴格的同步和并發控制的場景。
private synchronized void oneLock() {
//doSomething();
}
兩個線程t1和t2正在同時訪問該oneLock()方法。如果t1先獲取鎖并執行其中的同步代碼塊,并且 t2 也嘗試訪問oneLock() 方法,則它將被阻止,因為鎖由t1 持有。
在這種情況下,鎖處于稱為重量級鎖的狀態。
從上面的例子可以看出,t2由于無法獲取鎖,因此被掛起,等待t1釋放鎖后再被喚醒。
線程的掛起和喚醒涉及CPU內的上下文切換,這會產生很大的開銷。
由于這個過程的成本相對較高,具有這種行為的鎖被稱為重量級鎖。
什么是輕量級鎖
輕量級鎖是一種同步機制,旨在減輕與傳統重量級鎖(例如 JAVA synchronized關鍵字提供的鎖)相關的性能開銷。
繼續前面的示例,讓我們現在考慮t1和t2交替執行oneLock()方法。
在這種情況下,t1和t2不需要阻塞,因為它們之間沒有爭用。換句話說,不需要重量級的鎖。
當線程交替執行臨界區而不發生爭用時,這種場景下使用的鎖被稱為輕量級鎖。
輕量級鎖相對于重量級鎖的優點:
1、每次加鎖只需要一次CAS操作。
2. 無需分配ObjectMonitor對象。
3、線程不需要被掛起或喚醒。
什么是偏向鎖?
在只有一個線程(假設 t1)一致執行oneLock()方法的情況下,使用輕量級鎖t1在每次獲取鎖時執行 CAS 操作。這可能會導致一些性能開銷。
于是,偏向鎖的概念就出現了。
當鎖偏向特定線程時,該線程可以再次獲取鎖,而無需進行 CAS 操作。相反,簡單的比較就足以獲得鎖。這個過程非常高效。
偏向鎖相比輕量級鎖的優點:
- 當同一個線程多次獲取鎖時,不需要再次執行 CAS 操作。簡單的比較就足夠了。
怎樣加鎖?
讓我們從源代碼的角度深入研究一下 Java 中這些鎖是如何實現的。
鎖的本質在于共享變量,所以問題的關鍵是如何訪問這些共享變量。了解這一點就了解了這三種鎖的演變過程的一半。
接下來我將從源碼分析的角度重點介紹一下這些信息。
既然我們處理的是鎖,自然就涉及到鎖的獲取和釋放操作,而在偏向鎖的情況下,還有鎖撤銷操作。
對象頭是Java對象在內存中布局的一部分,用于存儲對象的元數據信息和鎖定狀態。
在深入源碼之前,我們先推測一下線程 t1 獲取偏向鎖的過程:
- 首先檢查Mark word中的線程ID是否有值。
- 如果沒有,則意味著還沒有線程獲得鎖。本例中,直接將t1的線程ID記錄到Mark Word中。多個線程可能會嘗試同時修改Mark Word,因此需要CAS操作來修改Mark Word。
- 如果已經有一個 ID 值,那么有兩種可能性:
- 如果該ID是t1的ID,那么本次鎖獲取就是一個可重入的過程,t1可以直接獲取鎖。
- 如果該ID不是t1的ID,則意味著另一個線程已經獲取了鎖。這種情況下,t1需要經過撤銷過程來獲取鎖。
CASE(_monitorenter): {
// 1. 獲取對象頭,表示為“oop”(普通對象指針)。
oop lockee = STACK_OBJECT(-1);
CHECK_NULL(lockee);
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
BasicObjectLock* entry = NULL;
while (most_recent != limit ) {
// 2. 遍歷線程棧找到對應的可用BasicObjectLock。
if (most_recent->obj() == NULL) entry = most_recent;
else if (most_recent->obj() == lockee) break;
most_recent++;
}
if (entry != NULL) {
// 3. BasicObjectLock 的 _obj 字段指向 oop。
entry->set_obj(lockee);
int success = false;
uintptr_t epoch_mask_in_place = (uintptr_t)markOopDesc::epoch_mask_in_place;
// 從對象頭中檢索標記
markOop mark = lockee->mark();
intptr_t hash = (intptr_t) markOopDesc::no_hash;
// 檢查是否支持偏向鎖定。
if (mark->has_bias_pattern()) {
uintptr_t thread_ident;
uintptr_t anticipated_bias_locking_value;
thread_ident = (uintptr_t)istate->thread();
// 4. 獲取異或運算的結果。
anticipated_bias_locking_value =
(((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) &
~((uintptr_t) markOopDesc::age_mask_in_place);
if (anticipated_bias_locking_value == 0) {
// 5. 如果相等,則認為是可重入獲取鎖。
if (PrintBiasedLockingStatistics) {
(* BiasedLocking::biased_lock_entry_count_addr())++;
}
success = true;
}
else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) {
// 6. 如果不支持偏向鎖
markOop header = lockee->klass()->prototype_header();
if (hash != markOopDesc::no_hash) {
header = header->copy_set_hash(hash);
}
// 執行CAS操作,將Mark Word修改為解鎖狀態。
if (Atomic::cmpxchg_ptr(header, lockee->mark_addr(), mark) == mark) {
if (PrintBiasedLockingStatistics)
(*BiasedLocking::revoked_lock_entry_count_addr())++;
}
}
else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) {
// 7. 如果epoch已過期,則使用當前線程的ID構造偏向鎖
markOop new_header = (markOop) ( (intptr_t) lockee->klass()->prototype_header() | thread_ident);
if (hash != markOopDesc::no_hash) {
new_header = new_header->copy_set_hash(hash);
}
if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), mark) == mark) {
if (PrintBiasedLockingStatistics)
(* BiasedLocking::rebiased_lock_entry_count_addr())++;
} else {
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
success = true;
}
else {
// 8. 構造一個匿名偏向鎖。
markOop header = (markOop) ((uintptr_t) mark & ((uintptr_t)markOopDesc::biased_lock_mask_in_place |
(uintptr_t)markOopDesc::age_mask_in_place |
epoch_mask_in_place));
if (hash != markOopDesc::no_hash) {
header = header->copy_set_hash(hash);
}
// 構造一個指向當前線程的偏向鎖。
markOop new_header = (markOop) ((uintptr_t) header | thread_ident);
DEBUG_ONLY(entry->lock()->set_displaced_header((markOop) (uintptr_t) 0xdeaddead);)
// 執行CAS操作將鎖修改為與當前線程關聯的偏向鎖。
if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) {
if (PrintBiasedLockingStatistics)
(* BiasedLocking::anonymously_biased_lock_entry_count_addr())++;
}
else {
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
success = true;
}
}
if (!success) {
// 如果嘗試使用偏向鎖不成功,系統會嘗試將鎖升級為輕量級鎖。
markOop displaced = lockee->mark()->set_unlocked();
entry->lock()->set_displaced_header(displaced);
bool call_vm = UseHeavyMonitors;
if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
entry->lock()->set_displaced_header(NULL);
} else {
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
}
}
UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
} else {
istate->set_msg(more_monitors);
UPDATE_PC_AND_RETURN(0); // Re-execute
}
}
代碼比較多,下面我將對代碼注釋中注釋1-8標注的內容進行詳細解釋。
# 1.oop代表對象頭,包含Mark Word和Klass Word。
# 2.BasicObjectLock的結構如下:
#basicLock.hpp
class BasicObjectLock VALUE_OBJ_CLASS_SPEC {
friend class VMStructs;
private:
BasicLock _lock;
oop _obj;
...
};
class BasicLock VALUE_OBJ_CLASS_SPEC {
friend class VMStructs;
private:
volatile markOop _displaced_header;
...
};
BasicObjectLock是著名的Lock Record的實現,它包括兩個元素:
- 存儲Mark Word的移位頭_displaced_header。
- 指向對象頭的指針:_obj。
# 3、將Lock Record中的_obj字段賦值給lockee,代表對象頭。
# 4. 從對象頭lockee中,檢索Klass Word,它是指向Klass類型的指針。在Klass類內部,有一個名為_prototype_header的字段,它也代表Mark Word。它存儲偏向鎖定標志之類的信息。
在此步驟中,提取此信息并將其與當前線程 ID 連接起來。
然后與對象頭中的Mark Word 執行XOR 運算。目標是識別不同的位。
后續步驟涉及確定Mark Word的哪些特定部分不相等,從而導致不同的處理邏輯。
# 5. 如果上面的異或運算結果相等,則表明Mark Word中包含當前線程ID,并且epoch和偏向鎖標志一致。
這表明該鎖已經被當前線程持有,表明是可重入的。由于線程已經擁有鎖,因此不需要采取進一步的操作。
# 6. 觀察Mark Word中的偏向鎖標志與Klass中的偏向鎖標志不一致,并且考慮到Mark Word已經被識別為具有偏向鎖,因此可以推斷Klass不再支持偏向鎖。
鑒于不支持偏向鎖定,標記字被修改以反映解鎖狀態。這為進一步升級到輕量級鎖定或重量級鎖定做好了準備。
# 7. 在識別出 Mark Word 中的紀元與 Klass 中的標記之間的差異后,可以推斷發生了批量重新偏置。這種情況下,直接修改Mark Word,使其偏向當前線程。
# 8、如果以上條件都不滿足,則表明是匿名偏向鎖(不偏向任何線程的偏向鎖)。在這種情況下,會嘗試直接修改Mark Word以偏向當前線程
總結
- 每次線程嘗試獲取鎖時,都需要關聯一個鎖記錄,并將“_obj”指針設置為對象頭。這在鎖定記錄和對象頭之間建立了連接。
- 一旦線程成功將自己的線程ID寫入Mark Word,就表明該線程已經獲得了偏向鎖。
在偏向鎖狀態下,鎖記錄和對象頭之間建立了關系。這種關系由指向對象頭的鎖定記錄的 _obj 字段表示。
我們回顧一下線程t1和t2獲取偏向鎖的過程:
- 線程 t1 嘗試獲取鎖。最初,鎖處于匿名偏向狀態, T1成功獲取鎖。
- 線程 t1 嘗試再次獲取鎖。由于它已經持有鎖,所以他會來獲取可重入鎖。
- 同時,線程 t2 嘗試獲取鎖。由于 t1 當前持有鎖定,因此 t2 會鎖撤銷。
鎖撤銷
如果嘗試獲取偏向鎖不成功,鎖將恢復為未鎖定狀態,然后升級為輕量級鎖。此過程稱為偏向鎖撤銷。
#InterpreterRuntime.cpp
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);
}
...
#synchronizer.cpp
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 {
assert(!attempt_rebias, "can not rebias toward VM thread");
// 在安全點執行撤銷。
BiasedLocking::revoke_at_safepoint(obj);
}
assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
}
slow_enter (obj, lock, THREAD) ;
}
可見,撤銷分為安全點撤銷和非安全點撤銷。
非安全點撤銷,也稱為“revoke_and_rebias”,發生在未等待安全點而撤銷偏向鎖時。在這個過程中,偏向鎖被直接撤銷,并且對象的標記字被更新以反映新的狀態,而不需要安全點來保證一致的狀態轉換。
當發生非安全點撤銷時,偏向鎖的狀態從偏向變為正常或可重偏向。
如果它更改為可重偏向狀態,則意味著如果另一個線程尋求該鎖,該鎖可以再次偏向。
這允許更快、更有效的鎖定轉換,因為如果另一個線程在撤銷后不久獲取該鎖,則該鎖可能會跳過中間狀態并直接進入偏向狀態。
從本質上講,非安全點撤銷減少了等待安全點的需要,并實現了更靈活、響應更靈敏的方法來撤銷偏向鎖,從而提高了性能并減少了某些場景下的鎖爭用。
批量重新偏向和批量撤銷
經過以上分析,我們了解到以下幾點:
- 當一個線程持有偏向鎖,而另一個線程試圖獲取該鎖時,需要撤銷該鎖。
- 撤銷過程首先嘗試使用CAS在非安全點將Mark Word更改為解鎖狀態。如果仍然無法實現撤銷,則可以考慮在安全點執行撤銷的選項,盡管在安全點執行撤銷的效率相對較低。
因此,偏向鎖引入了批量重偏向和批量撤銷的概念。
當對象的鎖被撤銷的次數達到一定閾值時,例如20次,就會觸發批量重偏邏輯。
這涉及到修改 Klass 中的標記以及當前使用的該類型鎖的 Mark Word 中的標記。
當線程嘗試獲取偏向鎖時,它會將當前對象的紀元值與 Klass 中的標記值進行比較。
如果不相等,則認為鎖已過期。在這種情況下,允許線程直接CAS修改Mark Word以偏向當前線程,避免撤銷邏輯。這對應于偏向鎖進入最初討論中的分析標簽(7)。
同樣,當撤銷次數達到40次時,就認為該對象不再適合偏向鎖。
因此,Klass 中的偏向鎖標志發生更改,以指示不再支持偏向鎖。
當線程嘗試獲取偏向鎖時,它會檢查 Klass 中的偏向鎖標志。如果不再允許偏差,則表明批次撤銷較早發生。
在這種情況下,允許線程直接CAS將Mark Word修改為解鎖狀態,避免了撤銷邏輯。這對應于偏向鎖進入最初討論中的分析標簽(6)。
批量重新偏向和批量撤銷是旨在提高偏向鎖定性能的優化。
鎖釋放
#bytecodeInterpreter.cpp
CASE(_monitorexit): {
oop lockee = STACK_OBJECT(-1);
CHECK_NULL(lockee);
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
// 遍歷線程棧
while (most_recent != limit ) {
// 查找對應的鎖記錄
if ((most_recent)->obj() == lockee) {
BasicLock* lock = most_recent->lock();
markOop header = lock->displaced_header();
// 將鎖定記錄中的_obj字段設置為null
most_recent->set_obj(NULL);
// 這是輕量級鎖的釋放。
UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
}
most_recent++;
}
...
}
您可能已經注意到,Mark Word 沒有改變;它仍然偏向于前一個線程。然而,鎖還沒有被釋放。事實上,當線程退出臨界區時,它不會釋放偏向鎖。
原因是:
當再次需要鎖時,簡單的按位比較就可以快速判斷是否是可重入獲取。這意味著不需要每次都執行CAS操作就可以高效地獲取鎖。這種效率是偏向鎖在只有一個線程訪問鎖的場景下的核心優勢。
總結
- 偏向鎖中的“鎖”指的是Mark Word。修改Mark Word是獲取鎖所必需的,由于潛在的多線程爭用,這可能會涉及CAS操作。
- 由于撤銷操作在安全點執行時效率可能較低,并且多次撤銷會進一步影響效率,因此引入了批量重偏和撤銷機制。
- 偏向鎖的可重入計數取決于線程堆棧中存在的鎖記錄的數量。
- 如果偏向鎖撤銷失敗,鎖最終會升級為輕量級鎖。
- 退出時,偏向鎖不會修改Mark Word,也就是說鎖沒有被釋放。
未完待續。。。。。