上篇寫完了ReentrantLock原始碼實現,從我們的角度分析設計鎖,在對比大神的實現,順道拍了一波道哥的馬屁,雖然他看不到,哈哈。這一篇我們來聊一聊synchronized的原始碼實現,並對比reentrantLock的實現,相信認真看完一定會對鎖的理解更加深入。
廢話不多說先來一段程式碼:
static String s = new String(); static int a = 1; public static void main(String[] args) { synchronized (s) { a++; } }
我們一般寫加鎖也就這麼寫,為啥一個synchronized關鍵字就能做到加鎖的效果呢,我們來看下這段程式碼main方法裡的位元組碼:
0: getstatic #2 // Field s:Ljava/lang/String; 3: dup 4: astore_1 5: monitorenter 6: getstatic #3 // Field a:I 9: iconst_1 10: iadd 11: putstatic #3 // Field a:I 14: aload_1 15: monitorexit 16: goto 24 19: astore_2 20: aload_1 21: monitorexit 22: aload_2 23: athrow 24: return
我們主要看一下標紅的幾行,5,15,21,其中monitorenter是進入臨界區的操作,monitorexit是退出臨界區時的操作,而為啥又倆monitorexit,其中15行是正常退出的,而21行是異常退出的,畢竟我們之前寫lock是放在finally中的,而這裡當然也要通過手段保證退出臨界區必須要釋放鎖。知道了synchronized其實就是monitorenter和monitorexit,我們還需要了解的就是物件頭,這裡我特意補了一篇物件頭的介紹:https://www.cnblogs.com/gmt-hao/p/14151951.html,接下來的synchronized原始碼還是比較依賴物件頭的理解的。
在翻看synchronized的原始碼時,找入口就花了我很長的時間,有的文章說是InterpreterRuntime::monitorenter方法,有的文章寫的是bytecodeInterpreter.cpp裡的CASE(_monitorenter)方法後來我發現後者裡面其實是呼叫了前者的,所以傾向於後者看了下去,但是我下載的jdk8裡面的程式碼是這樣的:
CASE(_monitorenter): { oop lockee = STACK_OBJECT(-1); //取到鎖物件 // derefing's lockee ought to provoke implicit null check CHECK_NULL(lockee); //判空 // find a free monitor or one already allocated for this object // if we find a matching object then we need a new monitor // since this is recursive enter BasicObjectLock* limit = istate->monitor_base(); BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base(); BasicObjectLock* entry = NULL; while (most_recent != limit ) { if (most_recent->obj() == NULL) entry = most_recent; else if (most_recent->obj() == lockee) break; most_recent++; } if (entry != NULL) { entry->set_obj(lockee); markOop displaced = lockee->mark()->set_unlocked(); //複製一份鎖物件並將其設定為無鎖狀態 entry->lock()->set_displaced_header(displaced); if (Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) { // Is it simple recursive case? if (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 } }
對,就這麼點就沒了,網上也有很多文章是按照這個寫的,這讓我看的時候很困惑,而且這些文章關於偏向鎖的解釋我都感覺和程式碼對不上,有種硬生生強塞進去的感覺,我當時就很好奇,這種文章的作者自己真的理解了嗎,寫這些文章不怕誤導讀者嗎。這裡我要推薦一篇我覺得寫的非常好的,我也從中借鑑了很多,看了這篇部落格之後,很多之前沒理通的邏輯都弄明白了,非常感謝,這裡貼上鍊接:https://www.jianshu.com/p/4758852cbff4(找資料的過程中發現很多部落格都是抄的這篇文章的),關於文章中寫到的為什麼jdk8的某版本及之前的版本里面程式碼無法解釋偏向鎖的問題,也是看了文章中推薦的R大的文章才算是有所理解,在此非常感謝這些真正傳道授業解惑的人,也貼一下連結:https://book.douban.com/annotation/31407691/,我就不再贅述(主要是理解的比較淺顯),希望想了解synchronized原始碼的小夥伴還是看一下上面提供的連結文章。
下面才是原始碼解析正式開始,由於bytecodeInterpreter.cpp中的CASE(_monitorenter)程式碼和真正模板直譯器的彙編程式碼邏輯基本一致(主要是我太菜了看不懂),這裡就用這裡的實現來解釋synchronized原始碼,貼上程式碼:
1.偏向鎖的獲取
CASE(_monitorenter): { //獲取鎖物件 oop lockee = STACK_OBJECT(-1);//棧幀當中生成一個lock record 記錄 BasicObjectLock* limit = istate->monitor_base(); BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base(); BasicObjectLock* entry = NULL; while (most_recent != limit ) { if (most_recent->obj() == NULL) entry = most_recent; else if (most_recent->obj() == lockee) break; most_recent++; } if (entry != NULL) { //將lock record中的obj指向鎖物件 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; // implies UseBiasedLocking //判斷是否禁用偏向鎖 if (mark->has_bias_pattern()) { uintptr_t thread_ident; uintptr_t anticipated_bias_locking_value; //獲取執行緒id thread_ident = (uintptr_t)istate->thread(); //計算是否偏向自己 anticipated_bias_locking_value = (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) & ~((uintptr_t) markOopDesc::age_mask_in_place); //1.判斷是否偏向自己 if (anticipated_bias_locking_value == 0) { // already biased towards this thread, nothing to do if (PrintBiasedLockingStatistics) { (* BiasedLocking::biased_lock_entry_count_addr())++; } success = true; } //2.判斷是否可偏向,不可偏向則嘗試撤銷 else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) { // 拿到鎖物件的原型物件頭 markOop header = lockee->klass()->prototype_header(); if (hash != markOopDesc::no_hash) { header = header->copy_set_hash(hash); } //其實這裡是將鎖物件的mark word替換為沒有偏向的,即撤銷偏向 if (lockee->cas_set_mark(header, mark) == mark) { if (PrintBiasedLockingStatistics) (*BiasedLocking::revoked_lock_entry_count_addr())++; } } //3.判斷是否過期,即判斷物件頭中epoch是否不一致,若不一致則嘗試重偏向 else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) { // try rebias //基於lockee物件構造一個偏向當前執行緒的mark word 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); } //cas替換mark word為上面偏向當前執行緒的 if (lockee->cas_set_mark(new_header, mark) == mark) { if (PrintBiasedLockingStatistics) (* BiasedLocking::rebiased_lock_entry_count_addr())++; } else { //替換失敗則說明有多個執行緒同時競爭,鎖升級 CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception); } success = true; } else {//4.走到這裡要麼是偏向其他執行緒且沒有過期的,要麼就是匿名偏向(即沒有儲存執行緒資訊) // try to bias towards thread in case object is anonymously biased //這裡構造一個當前mark word的匿名偏向物件頭 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); } //這是將mark word偏向當前執行緒 markOop new_header = (markOop) ((uintptr_t) header | thread_ident); // debugging hint DEBUG_ONLY(entry->lock()->set_displaced_header((markOop) (uintptr_t) 0xdeaddead);) //這裡會嘗試將偏向當前執行緒的mark word替換到鎖物件中, //若是匿名偏向則可以cas成功,若已經偏向其他執行緒, //或有可能剛好被其他執行緒先修改了,都說明有多個執行緒競爭, //則會cas失敗 if (lockee->cas_set_mark(new_header, header) == header) { if (PrintBiasedLockingStatistics) (* BiasedLocking::anonymously_biased_lock_entry_count_addr())++; } else {//替換失敗則說明有多個執行緒同時競爭,鎖升級 CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception); } success = true; } } // traditional lightweight locking if (!success) { //輕量鎖,當偏向鎖未開啟或失敗 //構造無鎖mark word markOop displaced = lockee->mark()->set_unlocked(); // 將上面構造的lock record指向該無鎖mark word entry->lock()->set_displaced_header(displaced); bool call_vm = UseHeavyMonitors; // 使用重量級鎖或者輕量級鎖加鎖失敗,結果都會導致使用重量級鎖 //這裡if條件不滿足的話則說明輕量級鎖加鎖成功直接結束 if (call_vm || lockee->cas_set_mark((markOop)entry, displaced) != displaced) { // Is it simple recursive case? //鎖重入 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 } }
上面的程式碼我都加了註釋,基本邏輯應該是可以看的懂得,主要解釋一下幾個難以理解的點,這裡我分別用紅黃藍三色標記了,下面一一解釋:
紅:這裡的BasicObjectLock的定義在basicLock.hpp中:
class BasicObjectLock { friend class VMStructs; private: BasicLock _lock; // the lock, must be double word aligned oop _obj;
裡面分別包含了一個BasicLock和一個oop物件,BasicLock的定義也在當前檔案中:
class BasicLock { friend class VMStructs; friend class JVMCIVMStructs; private: volatile markOop _displaced_header; public:
其實就是一個markOop物件頭,也就是說BasicObjectLock其實就是一個物件本身和物件頭的組合,也叫做lock record。
瞭解完這些我們再看程式碼,其實就是從當前呼叫方法棧的most_recent(棧底)搜尋到limit(棧頂)遍歷查詢,直到找到一個空閒的或者之前就指向當前鎖物件的lock record。
黃:黃色部分程式碼還是挺複雜的,首先看 (uintptr_t)lockee->klass()->prototype_header() | thread_ident) ,這個其實是那當前鎖物件的class原型物件頭和當前執行緒進行或運算(其實相當於物件頭記錄下當前執行緒資訊),。
再看後面 ^ (uintptr_t)mark,其實就是那生成的偏向當前執行緒的mark word和當前鎖物件的進行異或運算看兩者的區別。
再看後面& ~((uintptr_t) markOopDesc::age_mask_in_place) ,~((uintptr_t) markOopDesc::age_mask_in_place) 這個是將拿到mark word為...00001111000取反後變為...11110000111,再和前面進行與運算,可以排除掉gc年齡的干擾,就可以將不同點集中到偏向執行緒、偏向狀態以及鎖狀態上,如果上面程式碼中步驟1等於0成立則說明和鎖物件一樣,可以得出偏向自己,步驟2和偏向鎖狀態與運算不等於0說明偏向狀態標誌位為0,沒有開啟偏向模式,步驟3也一樣,只是計算epoch值是否相等,可判斷是否需要重偏向。
藍:上面也提到了這塊通過判斷epoch值是否相同,但是原因我找過很多資料,最終發現有一篇問題下的一個回答說的比較好,附上鍊接:https://www.zhihu.com/question/56582060/answer/155398235 防止回答失效,原文摘抄了過來:
上面主要介紹了偏向鎖的加鎖和輕量鎖的部分加鎖流程,流程可參考下圖:
2.偏向鎖批量撤銷和重偏向
接下來主要是偏向鎖撤銷和重偏向流程,先看InterpreterRuntime::monitorenter程式碼:
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))if (PrintBiasedLockingStatistics) { Atomic::inc(BiasedLocking::slow_path_entry_count_addr()); } Handle h_obj(thread, elem->obj()); assert(Universe::heap()->is_in_reserved_or_null(h_obj()), "must be NULL or an object"); if (UseBiasedLocking) { //開啟偏向鎖 // Retry fast entry if bias is revoked to avoid unnecessary inflation ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK); } else { ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK); }
這裡主要就兩個方法開啟偏向鎖時執行的fast_enter和未開啟偏向鎖執行的slow_enter,我們先看fast_enter方法:
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempBiasedLocking::revoke_and_rebiast_rebias, TRAPS) { if (UseBiasedLocking) { //使用偏向鎖 if (!SafepointSynchronize::is_at_safepoint()) { //不在安全點(安全點指所有java執行緒都停在安全點,只有vm執行緒執行) 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); } assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now"); } slow_enter(obj, lock, THREAD); }
這裡再次判斷了UseBiasedLocking,由於fast_enter方法裡面也會執行slow_enter方法,個人感覺上一層的if-else是多餘的,不過也有可能是為了可讀性更強吧。
這裡的is_at_safepoint我找了下原始碼裡的解釋,在safepoint.cpp檔案裡可以看到:
inline static bool is_at_safepoint() { return _state == _synchronized; } enum SynchronizeState { _not_synchronized = 0, // Threads not synchronized at a safepoint // Keep this value 0. See the comment in do_call_back() _synchronizing = 1, // Synchronizing in progress _synchronized = 2 // All Java threads are stopped at a safepoint. Only VM thread is running };
是否在安全點其實就是判斷狀態是不是_synchronized,而作者的解釋就是:所有java執行緒在安全點暫停,只有vm執行緒處於執行態,而我在讀revoke_and_rebias方法和revoke_at_safepoint方法原始碼發現區別主要是前者多了一些校驗和cas操作,因此此處只寫revoke_and_rebias的邏輯實現:
BiasedLocking::Condition BiasedLocking::revoke_and_rebias(Handle obj, bool attempt_rebias, TRAPS) { assert(!SafepointSynchronize::is_at_safepoint(), "must not be called while at safepoint"); markOop mark = obj->mark(); //物件頭 //是匿名偏向 && 重偏向標識為false 根據原文解釋可以知道這裡做的是當前鎖物件hashcode計算會撤銷偏向 if (mark->is_biased_anonymously() && !attempt_rebias) { markOop biased_value = mark; markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age()); markOop res_mark = obj->cas_set_mark(unbiased_prototype, mark); if (res_mark == biased_value) { return BIAS_REVOKED; } } else if (mark->has_bias_pattern()) { //開啟偏向模式 Klass* k = obj->klass(); markOop prototype_header = k->prototype_header(); //鎖物件對應的class物件頭關閉偏向,出現這種情況--看註釋這種情況是由於批量撤銷延遲,需要cas替換修復 if (!prototype_header->has_bias_pattern()) { markOop biased_value = mark; markOop res_mark = obj->cas_set_mark(prototype_header, mark);return BIAS_REVOKED; } else if (prototype_header->bias_epoch() != mark->bias_epoch()) { //epoch過期 markOop biased_value = mark; markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch()); markOop res_mark = obj->cas_set_mark(rebiased_prototype, mark); if (res_mark == biased_value) { return BIAS_REVOKED_AND_REBIASED; } } else { //false,cas偏向撤銷 markOop biased_value = mark; markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age()); markOop res_mark = obj->cas_set_mark(unbiased_prototype, mark); if (res_mark == biased_value) { return BIAS_REVOKED; } } } } //這裡是根據配置的允許撤銷和重偏向次數與真實次數對比返回對應的標識,並在下面執行對應操作 //enum HeuristicsResult { // HR_NOT_BIASED = 1, 不需要偏向 // HR_SINGLE_REVOKE = 2, 單個撤銷 // HR_BULK_REBIAS = 3, 批量重偏向 // HR_BULK_REVOKE = 4 批量撤銷 // }; HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias); if (heuristics == HR_NOT_BIASED) {//未偏向直接返回 return NOT_BIASED; } else if (heuristics == HR_SINGLE_REVOKE) { //執行單個撤銷 Klass *k = obj->klass(); markOop prototype_header = k->prototype_header(); if (mark->biased_locker() == THREAD && prototype_header->bias_epoch() == mark->bias_epoch()) { //判斷是執行緒自己並且epoch值沒有過期,撤銷及重偏向,直接執行撤銷偏向 ResourceMark rm; log_info(biasedlocking)("Revoking bias by walking my own stack:"); EventBiasedLockSelfRevocation event; BiasedLocking::Condition cond = revoke_bias(obj(), false, false, (JavaThread*) THREAD, NULL); ((JavaThread*) THREAD)->set_cached_monitor_info(NULL); assert(cond == BIAS_REVOKED, "why not?"); if (event.should_commit()) { post_self_revocation_event(&event, k); } return cond; } else { //走到這裡說明是其他執行緒,則必須等到安全點時由vm執行緒執行 EventBiasedLockRevocation event; VM_RevokeBias revoke(&obj, (JavaThread*) THREAD); VMThread::execute(&revoke); if (event.should_commit() && revoke.status_code() != NOT_BIASED) { post_revocation_event(&event, k, &revoke); } return revoke.status_code(); } } EventBiasedLockClassRevocation event; //批量撤銷 VM_BulkRevokeBias bulk_revoke(&obj, (JavaThread*) THREAD, (heuristics == HR_BULK_REBIAS), attempt_rebias); VMThread::execute(&bulk_revoke);return bulk_revoke.status_code(); }
這裡我已經刪了一部分註釋和斷言,但邏輯依舊感覺很複雜,流程上面程式碼寫了註釋,下面也會貼出流程圖,這裡主要解釋幾個重點:
1.撤銷偏向先生成一個未偏向mark word,然後cas替換, 重偏向則是生成一個儲存要替換的物件頭的mark word,然後cas替換。
2.HeuristicsResult heuristics = update_heuristics(obj(), attempt_rebias); 詳細分析:
static HeuristicsResult update_heuristics(oop o, bool allow_rebias) { markOop mark = o->mark(); if (!mark->has_bias_pattern()) { //不可偏向直接返回 return HR_NOT_BIASED; } Klass* k = o->klass(); jlong cur_time = os::javaTimeMillis(); //當前時間 jlong last_bulk_revocation_time = k->last_biased_lock_bulk_revocation_time(); //上次批量撤銷的時間 int revocation_count = k->biased_lock_revocation_count(); //撤銷的次數(之前看到撤銷的時候就會count++這裡用上了) //定義在globs.hpp,BiasedLockingBulkRebiasThreshold取值為20;BiasedLockingBulkRevokeThreshold取值為40,BiasedLockingDecayTime為25000毫秒 //這個if的邏輯還是比較簡單的,其實就是在指定時間閾值到達後,重偏向次數達到但撤銷未達到閾值,則執行預設的批量重偏向,並重置撤銷count值 if ((revocation_count >= BiasedLockingBulkRebiasThreshold) && //1.重偏向次數達到閾值 (revocation_count < BiasedLockingBulkRevokeThreshold) && //2.撤銷未到達閾值 (last_bulk_revocation_time != 0) && //這裡主要是為了防止多個執行緒同時觸發 (cur_time - last_bulk_revocation_time >= BiasedLockingDecayTime)) { //3.距上次批量撤銷時間是否達到閾值 k->set_biased_lock_revocation_count(0); revocation_count = 0; } // Make revocation count saturate just beyond BiasedLockingBulkRevokeThreshold if (revocation_count <= BiasedLockingBulkRevokeThreshold) { //此次的count值也要加上 revocation_count = k->atomic_incr_biased_lock_revocation_count(); } if (revocation_count == BiasedLockingBulkRevokeThreshold) { //達到批量撤銷閾值,返回對應標識,後續會執行批量撤銷 return HR_BULK_REVOKE; } if (revocation_count == BiasedLockingBulkRebiasThreshold) { //達到批量重偏向閾值,返回重偏向標識,後續會執行批量重偏向 return HR_BULK_REBIAS; } return HR_SINGLE_REVOKE; }
其實就是根據配置的閾值和實際重偏向或撤銷的次數比較,返回對應的標識列舉,並在後面的邏輯中做對應的處理(單個撤銷、批量撤銷、直接返回)。
3.無論是單個撤銷執行的revoke_bias或者批量撤銷執行的bulk_revoke其實最終執行的都是revoke_bias,後者也就是for迴圈處理了一下,所以這裡只分析revoke_bias邏輯:
static BiasedLocking::Condition revoke_bias(oop obj, bool allow_rebias, bool is_bulk, JavaThread* requesting_thread, JavaThread** biased_locker) { markOop mark = obj->mark(); if (!mark->has_bias_pattern()) { //沒有開啟偏向模式,直接返回NOT_BIASEDreturn BiasedLocking::NOT_BIASED; } uint age = mark->age(); markOop biased_prototype = markOopDesc::biased_locking_prototype()->set_age(age); //構造一個匿名偏向mark word markOop unbiased_prototype = markOopDesc::prototype()->set_age(age); //構建一個關閉偏向的(無鎖)mark word JavaThread* biased_thread = mark->biased_locker(); if (biased_thread == NULL) { //看註釋的意思是:匿名偏向物件,我們可能會因為在計算hashcode進入這一步 // Object is anonymously biased. We can get here if, for // example, we revoke the bias due to an identity hash code // being computed for an object. if (!allow_rebias) { //不允許偏向,設為無鎖mark word obj->set_mark(unbiased_prototype); }// Handle case where the thread toward which the object was biased has exited bool thread_is_alive = false; if (requesting_thread == biased_thread) { //當前執行緒和偏向執行緒一致 thread_is_alive = true; } else { //不一致,jvm會儲存一份存活執行緒list,這裡找偏向執行緒,找不到false,找到為true ThreadsListHandle tlh; thread_is_alive = tlh.includes(biased_thread); } if (!thread_is_alive) { //偏向執行緒不存活,允許重偏向則設為匿名偏向,否則設為無鎖 if (allow_rebias) { obj->set_mark(biased_prototype); } else { obj->set_mark(unbiased_prototype); }return BiasedLocking::BIAS_REVOKED; }//走到這裡執行緒還存活 GrowableArray<MonitorInfo*>* cached_monitor_info = get_or_compute_monitor_info(biased_thread);//根據偏向執行緒找到所有物件 BasicLock* highest_lock = NULL; //這個應該很熟悉了,上面介紹過,lock record中的物件頭部分 //遍歷找到的所有物件,找到當前物件(這塊不知道理解的對不對) for (int i = 0; i < cached_monitor_info->length(); i++) { MonitorInfo* mon_info = cached_monitor_info->at(i); if (oopDesc::equals(mon_info->owner(), obj)) {// Assume recursive case and fix up highest lock later markOop mark = markOopDesc::encode((BasicLock*) NULL); highest_lock = mon_info->lock(); highest_lock->set_displaced_header(mark); } else { } } if (highest_lock != NULL) { //這塊偏向鎖的理解還是有點問題,希望有大神可以指點指點 highest_lock->set_displaced_header(unbiased_prototype); //設定匿名偏向mark word obj->release_set_mark(markOopDesc::encode(highest_lock)); } else { //說明已經不在臨界區內if (allow_rebias) { obj->set_mark(biased_prototype); } else { obj->set_mark(unbiased_prototype); } } // If requested, return information on which thread held the bias if (biased_locker != NULL) { *biased_locker = biased_thread; } return BiasedLocking::BIAS_REVOKED; }
這裡主要做的就是判斷是否需要撤銷偏向,再根據是否在臨界區內以及是否允許偏向分別指向匿名偏向或無鎖mark word,這裡可以看的出來,如果是輕量鎖的話,撤銷之後還要指向一個lock record來儲存之前的mark word資訊,具體流程圖如下:
上面總的流程圖如下:
這張圖不知道為什麼試過很多方法,都是模糊的,這裡給個連結吧:https://www.processon.com/view/link/6010043c079129045d3a376a
3.輕量級鎖
其實上面講完就快到重量級鎖了,是不是突然發現輕量級鎖沒啥存在感,我也是寫重量級鎖寫著寫著發現輕量級鎖沒寫,這裡簡單補一下吧,其實輕量級鎖加鎖主要存在三個地方:
1.上面第一部分最後 !success的流程
2.第二部分就是遍歷物件monitor,然後設定lock record,但是具體的我理解的不太好,希望有大牛看到的話可以幫忙在評論區解釋一下
3.slow_enter裡面當mark->is_neutral()為true的時候,嘗試加偏向鎖,其實這裡就可以看的出來,偏向鎖其實就是將lock record(這個其實就是偏向鎖進來的時候建立的entry,可以回頭找一下)指向鎖物件,然後cas將鎖物件的mark word指向lock record,執行緒每次進來都會生成一個新的lock record,我相信看到這裡應該已經有了一個關係圖在腦子裡了:
大致是這樣的,雖有點粗糙,但大致能表達意思了,哈哈,偷個懶,等我重量級鎖和鎖釋放寫完,看有沒有精力再來優化一波吧。
寫重量鎖的時候,發現有的狀態看單詞不太看得懂,如上面的mark->is_neutral()翻譯是中立,這裡解釋一下,其實這裡可以看markOop.hpp檔案中有這麼幾個列舉:
enum { locked_value = 0, //00
unlocked_value = 1, //01
monitor_value = 2, //10
marked_value = 3, //11
biased_lock_pattern = 5 //101
};
光看0,1,2,3,5可能不太理解,但看後面的二進位制數,000輕量鎖,01無鎖,10重量鎖,11gc,101偏向鎖,而像is_neutral方法其實就是判斷是否是unlocked_value即無鎖,其他以此類推。
4.重量級鎖
qq重量級鎖主要分為鎖膨脹過程和加鎖過程,首先看slow_enter方法:
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { markOop mark = obj->mark();if (mark->is_neutral()) { //001 無鎖狀態,嘗試cas指向lock record 輕量鎖加鎖,成功直接返回 lock->set_displaced_header(mark); if (mark == obj()->cas_set_mark((markOop) lock, mark)) { return; } // Fall through to inflate() ... } else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { //輕量鎖重入 assert(lock != mark->locker(), "must not re-lock the same lock"); assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock"); lock->set_displaced_header(NULL); return; }//這裡設定一個不為0即不能看起來像重入標識,也不能看起來向持有鎖的值 lock->set_displaced_header(markOopDesc::unused_mark()); //重量鎖,inflate膨脹,enter真正加鎖 ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD); }
這裡做輕量級鎖最後的掙扎,畢竟重量級鎖就得進入核心態了,那消耗就大得多了。
接下來看膨脹流程:
ObjectMonitor* ObjectSynchronizer::inflate(Thread * Self, oop object, const InflateCause cause) { // Inflate mutates the heap ... // Relaxing assertion for bug 6320749. assert(Universe::verify_in_progress() || !SafepointSynchronize::is_at_safepoint(), "invariant"); EventJavaMonitorInflate event; for (;;) { //自旋 const markOop mark = object->mark(); assert(!mark->has_bias_pattern(), "invariant"); // The mark can be in one of the following states: //這裡分為以下幾種情況 // * Inflated - just return //膨脹完成,直接返回 // * Stack-locked - coerce it to inflated //輕量級加鎖狀態 // * INFLATING - busy wait for conversion to complete //膨脹中 // * Neutral - aggressively inflate the object. //無鎖狀態 // * BIASED - Illegal. We should never see this //偏向狀態,不過在這裡是非法的,不會出現 // CASE: inflated 直接返回 if (mark->has_monitor()) { ObjectMonitor * inf = mark->monitor(); assert(inf->header()->is_neutral(), "invariant"); assert(oopDesc::equals((oop) inf->object(), object), "invariant"); assert(ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is invalid"); return inf; } // CASE: inflation in progress - inflating over a stack-lock.//膨脹過程中,這裡只會有一個執行緒完成膨脹,其他執行緒呼叫spin/yield/park函式等待 if (mark == markOopDesc::INFLATING()) { ReadStableMark(object); continue; } // CASE: stack-locked if (mark->has_locker()) { //輕量級鎖加鎖狀態 ObjectMonitor * m = omAlloc(Self); //分配一個ObjectMonitor物件例項 m->Recycle(); m->_Responsible = NULL; m->_recursions = 0; m->_SpinDuration = ObjectMonitor::Knob_SpinLimit; // Consider: maintain by type/class markOop cmp = object->cas_set_mark(markOopDesc::INFLATING(), mark); //嘗試設定為INFLATING狀態,失敗則繼續下一輪自旋 if (cmp != mark) { omRelease(Self, m, true); continue; // Interference -- just retry } markOop dmw = mark->displaced_mark_helper(); assert(dmw->is_neutral(), "invariant"); // Setup monitor fields to proper values -- prepare the monitor m->set_header(dmw); m->set_owner(mark->locker()); m->set_object(object); guarantee(object->mark() == markOopDesc::INFLATING(), "invariant"); object->release_set_mark(markOopDesc::encode(m)); OM_PERFDATA_OP(Inflations, inc()); if (log_is_enabled(Debug, monitorinflation)) { if (object->is_instance()) { ResourceMark rm; log_debug(monitorinflation)("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s", p2i(object), p2i(object->mark()), object->klass()->external_name()); } } if (event.should_commit()) { post_monitor_inflate_event(&event, object, cause); } return m; }
膨脹可能會出現的這幾種狀態:
// * Inflated - just return //膨脹完成,直接返回 // * Stack-locked - coerce it to inflated //輕量級加鎖狀態, // * INFLATING - busy wait for conversion to complete //膨脹中 // * Neutral - aggressively inflate the object. //無鎖狀態
1.如果已經膨脹完成,則直接返回ObjectMonitor物件
2.如果當前狀態為膨脹過程中,這裡只會有一個執行緒去膨脹,其他執行緒呼叫spin/yield/park函式等待
3.如果當前處於輕量級鎖加鎖狀態,
(1)首先分配一塊給ObjectMonitor物件例項,然後cas設定mark word為INFLATING狀態,值為0,只有這一種情況會將mark word設定為0(看解釋是為了保證在膨脹過程中,防止其他執行緒來修改,同時也為了防止膨脹過程中解鎖,無論哪種情況,都會等待膨脹完成)
(2)設定ObjectMonitor物件指向lock record
(3)設定鎖物件指向mark word指向ObjectMonitor物件
4.如果當前處於無鎖狀態,和3差不多,但是不用設定INFLATING狀態,我個人覺得是因為無鎖狀態能走到這裡肯定是因為關閉了偏向鎖和輕量鎖,走到這的是第一個執行緒,能cas成功的也只有一個,如果失敗了重試就可以,不會有影響。
最終成功都會設定為重量鎖狀態。
膨脹完就該到真正的重量鎖加鎖了:
重量級鎖加鎖真的是繞到我了,光順著流程看程式碼壓根理不清楚,廢話不多說,先看為敬,看不懂的可以先去看看我前面的ReentrantLock原始碼分析:https://www.cnblogs.com/gmt-hao/p/14125742.html
void ObjectMonitor::enter(TRAPS) { Thread * const Self = THREAD; //null代表無鎖狀態,如果可以cas成功,則說明直接拿到鎖了(加鎖前先嚐試拿鎖,是不是和ReentrantLock似曾相識的感覺) //這種情況一般是關閉偏向鎖,輕量鎖 void * cur = Atomic::cmpxchg(Self, &_owner, (void*)NULL); if (cur == NULL) { //拿到鎖直接返回 return; } if (cur == Self) { //是否執行緒重入 _recursions++; return; } //這裡是判斷當前執行緒地址在不在當前棧記憶體區域,在的話則說明執行緒是之前持有過輕量級鎖的,並且是第一次進入重量級鎖, //這裡設計的很巧妙,第一次進入直接返回,不加鎖(這裡主要指的是不排隊),和ReentrantLock很相似,ReentrantLock也是第一次不排隊 if (Self->is_lock_owned ((address)cur)) { _recursions = 1; _owner = Self; //_owner指向當前獲取到鎖資源的執行緒 return; } Self->_Stalled = intptr_t(this); if (TrySpin(Self) > 0) { //嘗試一定次數的自旋,拿到鎖直接返回 Self->_Stalled = 0; return; } Atomic::inc(&_count); JFR_ONLY(JfrConditionalFlushWithStacktrace<EventJavaMonitorEnter> flush(jt);) EventJavaMonitorEnter event; if (event.should_commit()) { event.set_monitorClass(((oop)this->object())->klass()); event.set_address((uintptr_t)(this->object_addr())); } 。。。 { JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this); Self->set_current_pending_monitor(this); 。。。 for (;;) { jt->set_suspend_equivalent(); 。。。 EnterI(THREAD); if (!ExitSuspendEquivalent(jt)) break; _recursions = 0; _succ = NULL; exit(false, Self); jt->java_suspend_self(); } Self->set_current_pending_monitor(NULL); 。。。 }
去除掉七七八八不重要的程式碼如上,前面都是盡全力不進入等待佇列,直接看下面EnterI(THREAD)方法:
void ObjectMonitor::EnterI(TRAPS) { Thread * const Self = THREAD; // Try the lock - TATAS if (TryLock (Self) > 0) { //不死心再試一次 return; } if (TrySpin(Self) > 0) { //不死心再自旋一次 return; } //正式開始重量鎖 ObjectWaiter node(Self); Self->_ParkEvent->reset(); node._prev = (ObjectWaiter *) 0xBAD; node.TState = ObjectWaiter::TS_CXQ; ObjectWaiter * nxt; for (;;) { //自旋嘗試將node節點插入cxq頭部 node._next = nxt = _cxq; if (Atomic::cmpxchg(&node, &_cxq, nxt) == nxt) break; // Interference - the CAS failed because _cxq changed. Just retry. // As an optional optimization we retry the lock. //每次失敗都會去嘗試獲取一下鎖,看到這裡再結合上面也知道,synchronized是非公平鎖了 if (TryLock (Self) > 0) {return; } } //這裡可以結合ReentrantLock理解,就是當等待佇列為空時,嘗試設定為自己 if (nxt == NULL && _EntEnryList == NULL) { Atomic::replace_if_null(Self, &_Responsible); } int nWakeups = 0; int recheckInterval = 1; for (;;) { //一有機會就去嘗試 if (TryLock(Self) > 0) break; // park self if (_Responsible == Self) { //如果等待佇列為空,則只park自己一小段時間,過一會再次嘗試 Self->_ParkEvent->park((jlong) recheckInterval); // Increase the recheckInterval, but clamp the value. recheckInterval *= 8; if (recheckInterval > MAX_RECHECK_INTERVAL) { recheckInterval = MAX_RECHECK_INTERVAL; } } else { //否則park住自己,等待喚醒 Self->_ParkEvent->park(); } if (TryLock(Self) > 0) break; if (_succ == Self) _succ = NULL; // Invariant: after clearing _succ a thread *must* retry _owner before parking. OrderAccess::fence(); }
這裡的cxq佇列其實相當於是ReentrantLock的阻塞佇列,只不過reentrantLock會頭節點喚醒下一個節點,這裡是新執行緒塞入頭部,喚醒尾部,可以看到,基本是一致的,其實只要理解了ReentrantLock,可以發現,synchronized的加鎖和它基本是一樣的,這裡有一個 _EntEnryList干擾了我很久,後來慢慢找資料,發現其實和cxq是一樣的,只不過為了防止併發過高(據網上很多資料統計是這麼說的),會講一些候選node放入_EntEnryList,其實道理是一樣的,至於如何塞進去的,我們接著看wait,notify:
void ObjectMonitor::wait(jlong millis, bool interruptible, TRAPS) { Thread * const Self = THREAD ; JavaThread *jt = (JavaThread *)THREAD; TEVENT (Wait) ; 。。。。 ObjectWaiter node(Self); node.TState = ObjectWaiter::TS_WAIT ; //設定狀態為TS_WAIT Self->_ParkEvent->reset() ; OrderAccess::fence(); // ST into Event; membar ; LD interrupted-flag Thread::SpinAcquire (&_WaitSetLock, "WaitSet - add") ; AddWaiter (&node) ; //主要看這裡,將node放入等待佇列 Thread::SpinRelease (&_WaitSetLock) ; 。。。。 int ret = OS_OK ; int WasNotified = 0 ; { // State transition wrappers OSThread* osthread = Self->osthread(); OSThreadWaitState osts(osthread, true); { ThreadBlockInVM tbivm(jt); // Thread is in thread_blocked state and oop access is unsafe. jt->set_suspend_equivalent(); if (interruptible && (Thread::is_interrupted(THREAD, false) || HAS_PENDING_EXCEPTION)) { // Intentionally empty } else if (node._notified == 0) { //這裡直接park住 if (millis <= 0) { Self->_ParkEvent->park () ; } else { ret = Self->_ParkEvent->park (millis) ; } } } // Exit thread safepoint: transition _thread_blocked -> _thread_in_vm if (node.TState == ObjectWaiter::TS_WAIT) { Thread::SpinAcquire (&_WaitSetLock, "WaitSet - unlink") ; if (node.TState == ObjectWaiter::TS_WAIT) { DequeueSpecificWaiter (&node) ; // unlink from WaitSet assert(node._notified == 0, "invariant"); node.TState = ObjectWaiter::TS_RUN ; } Thread::SpinRelease (&_WaitSetLock) ; } guarantee (node.TState != ObjectWaiter::TS_WAIT, "invariant") ; OrderAccess::loadload() ; if (_succ == Self) _succ = NULL ; WasNotified = node._notified ; ObjectWaiter::TStates v = node.TState ; if (v == ObjectWaiter::TS_RUN) { //這裡等到喚醒之後會重新進入搶鎖 enter (Self) ; } else { guarantee (v == ObjectWaiter::TS_ENTER || v == ObjectWaiter::TS_CXQ, "invariant") ; ReenterI (Self, &node) ; node.wait_reenter_end(this); } } // OSThreadWaitState() jt->set_current_waiting_monitor(NULL); guarantee (_recursions == 0, "invariant") ; _recursions = save; // restore the old recursion count _waiters--; // decrement the number of waiters if (SyncFlags & 32) { OrderAccess::fence() ; } // check if the notification happened if (!WasNotified) { if (interruptible && Thread::is_interrupted(Self, true) && !HAS_PENDING_EXCEPTION) { TEVENT (Wait - throw IEX from epilog) ; THROW(vmSymbols::java_lang_InterruptedException()); } } }
其實這裡一共就幾步:1.加入waitSet佇列 2.park住 3.等待notify之後,加入EntryList或者cxq佇列,等待機會喚醒競爭鎖(enter方法上面講過)
這裡加入waitSet佇列其實跟ReentrantLock的Condition差不多,程式碼如下:
inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) { if (_WaitSet == NULL) { _WaitSet = node; node->_prev = node; node->_next = node; } else { ObjectWaiter* head = _WaitSet; ObjectWaiter* tail = head->_prev; tail->_next = node; head->_prev = node; node->_next = head; node->_prev = tail; } }
就是一個雙端佇列的頭節點新增過程,我們接下來再看一下notify方法:
首先看jdk1.8的:
void ObjectMonitor::notify(TRAPS) { CHECK_OWNER(); if (_WaitSet == NULL) { TEVENT (Empty-Notify) ; return ; } DTRACE_MONITOR_PROBE(notify, this, object(), THREAD); int Policy = Knob_MoveNotifyee ; Thread::SpinAcquire (&_WaitSetLock, "WaitSet - notify") ; ObjectWaiter * iterator = DequeueWaiter() ; if (iterator != NULL) { if (Policy != 4) { iterator->TState = ObjectWaiter::TS_ENTER ; } iterator->_notified = 1 ; Thread * Self = THREAD; iterator->_notifier_tid = Self->osthread()->thread_id(); ObjectWaiter * List = _EntryList ; if (Policy == 0) { // prepend to EntryList //看了很多網上的文章,對這塊都沒有解釋,其實無論是否為空,都是放到EntryList的頭部,這塊判斷只是jvm原始碼對於效能的追求而已 if (List == NULL) { iterator->_next = iterator->_prev = NULL ; _EntryList = iterator ; } else { List->_prev = iterator ; iterator->_next = List ; iterator->_prev = NULL ; _EntryList = iterator ; } } else if (Policy == 1) { // append to EntryList //這裡都是放到EntryList隊尾 if (List == NULL) { iterator->_next = iterator->_prev = NULL ; _EntryList = iterator ; } else { // CONSIDER: finding the tail currently requires a linear-time walk of // the EntryList. We can make tail access constant-time by converting to // a CDLL instead of using our current DLL. ObjectWaiter * Tail ; for (Tail = List ; Tail->_next != NULL ; Tail = Tail->_next) ; assert (Tail != NULL && Tail->_next == NULL, "invariant") ; Tail->_next = iterator ; iterator->_prev = Tail ; iterator->_next = NULL ; } } else if (Policy == 2) { // 放入cxq隊首 // prepend to cxq if (List == NULL) { iterator->_next = iterator->_prev = NULL ; _EntryList = iterator ; } else { iterator->TState = ObjectWaiter::TS_CXQ ; for (;;) { ObjectWaiter * Front = _cxq ; iterator->_next = Front ; if (Atomic::cmpxchg_ptr (iterator, &_cxq, Front) == Front) { break ; } } } } else if (Policy == 3) { // 放入cxq隊尾 iterator->TState = ObjectWaiter::TS_CXQ ; for (;;) { ObjectWaiter * Tail ; Tail = _cxq ; if (Tail == NULL) { iterator->_next = NULL ; if (Atomic::cmpxchg_ptr (iterator, &_cxq, NULL) == NULL) { break ; } } else { while (Tail->_next != NULL) Tail = Tail->_next ; Tail->_next = iterator ; iterator->_prev = Tail ; iterator->_next = NULL ; break ; } } } else { ParkEvent * ev = iterator->_event ; iterator->TState = ObjectWaiter::TS_RUN ; OrderAccess::fence() ; ev->unpark() ; } if (Policy < 4) { iterator->wait_reenter_begin(this); } } Thread::SpinRelease (&_WaitSetLock) ; if (iterator != NULL && ObjectMonitor::_sync_Notifications != NULL) { ObjectMonitor::_sync_Notifications->inc() ; } }
jdk1.8的比較複雜,具體情況主要根據Policy的值進行區分,0:放入EntryList頭部,1.放到EntryList隊尾 2.放到cxq隊首, 3.放入cxq隊尾,我猜這塊估計就是為了處理高併發下的競爭,不同的狀態代表有沒有執行緒在競爭。
再看jdk12的:
void ObjectMonitor::INotify(Thread * Self) { Thread::SpinAcquire(&_WaitSetLock, "WaitSet - notify"); ObjectWaiter * iterator = DequeueWaiter(); if (iterator != NULL) { iterator->TState = ObjectWaiter::TS_ENTER; iterator->_notified = 1; iterator->_notifier_tid = JFR_THREAD_ID(Self); ObjectWaiter * list = _EntryList; if (list == NULL) { iterator->_next = iterator->_prev = NULL; _EntryList = iterator; } else { iterator->TState = ObjectWaiter::TS_CXQ; for (;;) { ObjectWaiter * front = _cxq; iterator->_next = front; if (Atomic::cmpxchg(iterator, &_cxq, front) == front) { break; } } } iterator->wait_reenter_begin(this); } Thread::SpinRelease(&_WaitSetLock); }
這程式碼量,完全不是一個數量級的,jdk12的只判斷_EntryList是否為空來選擇放入_cxq還是_EntryList,其實我個人覺得,要理解synchronized的話,這個就足夠了。
總結:
總算是寫完了加鎖流程,先來總結一下吧,我們預設在開啟偏向鎖和輕量鎖的情況下,當執行緒進來時,首先會加上偏向鎖,其實這裡只是用一個狀態來控制(就和aqs的思想比較類似),會記錄枷鎖的執行緒,如果重入,則不會進行鎖升級,而當有多個執行緒交替進入(沒有產生鎖競爭),則會進行鎖升級,撤銷偏向鎖,而當多次撤銷同一個執行緒達到批量重偏向的閾值但沒有達到批量撤銷的閾值,則會進行批量重偏向,將輕量鎖重新設定為偏向鎖,並將撤銷及重偏向計數清0。當有多個鎖競爭時則會升級為重量級鎖,重量級鎖正常會進入一個cxq的佇列,在呼叫wait方法之後,則會進入一個waitSet的佇列park等待,而當呼叫notify方法喚醒之後,則有可能進入EntryList,其實這塊只是synchronized對於併發競爭資源的處理,可以不用過多的關注。
看完這些是不是感覺和ReentrantLock差不多,其實兩者的實現基本上是一致的,在重量級鎖上最後都會park進入核心態,而對於併發競爭資源的處理,導致synchronized的效率可能會比ReentrantLock更高,但是我感覺這種提升微乎其微,但是這也支援了說synchronized效率更高的證據吧。
寫這篇文章真的是太累了,網上相關資料很少,抄襲氾濫,一個錯誤,各個都是錯的,其實如果能多一些自己的研究和理解,對照一番也好,由此可見生態對於學習來說多重要.寫這一篇文章雖然不敢保證正確,但是很多地方還是自己好好的去研究了一番的,更多的是提供一個思路,最後,感謝上文引用到的文章作者給我提供了很多幫助。