Java Synchronized的偏向鎖詳細分析
上篇文章已經對Synchronized關鍵字做了初步的介紹,從字節(jié)碼層面介紹了Synchronized關鍵字,最終字節(jié)碼層面就是monitorenter和monitorexit字節(jié)碼指令。并且拿Synchronized關鍵字和Java的JUC包下的ReentrantLock做了比較。Synchronized關鍵字的初體驗-超鏈接地址
那么本篇文章將開始深入解析Synchronized關鍵字的底層原理,也就是解析Hotspot虛擬機對monitorenter和monitorexit字節(jié)碼指令的實現(xiàn)原理。
理論知識
相信各位讀者在準備面試中,都會背到關于Synchronized關鍵字的面試題,什么對象頭、鎖標志位、偏向鎖、輕量級鎖、重量級鎖,鎖升級的過程等等面試題。而對于一些不僅僅只想漂浮于表面的讀者來說,去看Synchronized底層源碼,只能說是一頭霧水。所以筆者有考慮這方面,所以理論知識(給臨時抱佛腳背理論的讀者)和底層源碼(給喜歡研究底層源碼的讀者)都會在這個系列中。
偏向鎖存在的意義:
先從字面意思來解釋,偏向于某個線程,是不是可以理解為偏向的這個線程獲取鎖都很效率呢?那么為什么要存在偏向鎖呢?讀者需要明白,任何框架存在的意義不僅僅是為了某一部分場景,肯定需要適配大部分場景,而Synchronized關鍵字使用的場景可能并發(fā)高,可能并發(fā)低,可能幾乎不存在并發(fā),所以實現(xiàn)者需要幫用戶去適配不同的場景,達到效率最高化。而對于幾乎不存在并發(fā)的場景,是不是可以理解為幾乎只有一個線程拿到Synchronized鎖,所以就存在偏向鎖去優(yōu)化這種場景,不讓所有場景都去走很復雜的邏輯。
偏向鎖實現(xiàn)的流程:
- 拿到鎖競爭對象
- 從當前線程棧中獲取到一個沒有使用的BasicObjectLock(用于記錄鎖狀態(tài))
- 查看當前是否開啟了偏向鎖模式
- 查看當前偏向鎖是否偏向的是當前線程,如果偏向的是當前線程,直接退出(可以理解成命中緩存)
- 查看當前是否已經鎖升級了,并且嘗試撤銷偏向鎖(想象一下并發(fā)過程中,可能其他線程已經完成了鎖對象的鎖升級)
- 當前epoch是否發(fā)生了改變,如果發(fā)生了改變,當前線程可以嘗試獲取偏向鎖,嘗試成功直接退出
- 當前是否是匿名偏向,或者已經偏向于某個線程,但是不是當前線程,此時可以嘗試獲取鎖,獲取成功直接退出
- 如果不支持偏向鎖或者第5步的撤銷偏向鎖失敗了,此時嘗試膨脹成輕量級鎖,如果輕量級鎖膨脹失敗了就繼續(xù)往上鎖膨脹
流程圖如下(僅只有偏向鎖邏輯)
源碼論證
首先,我們先需要知道Synchronized底層源碼的入口在哪里,在字節(jié)碼層面表示為monitorenter和monitorexit字節(jié)碼指令,而我們知道JVM是負責執(zhí)行字節(jié)碼,最終轉換成不同CPU平臺的ISA指令集(也稱之為跨平臺)。而JVM執(zhí)行字節(jié)碼分為
- CPP解釋執(zhí)行
- 模板解釋執(zhí)行(匯編)
- JIT編譯執(zhí)行
一級一級的優(yōu)化,而最根本是CPP解釋執(zhí)行,后者都是基于CPP解釋執(zhí)行的不斷優(yōu)化,后者的難度極大,所以讀者弄明白CPP解釋執(zhí)行就即可。
在Hotspot源碼中,CPP解釋執(zhí)行的入口在bytecodeInterpreter.cpp文件(這里要注意,JDK1.8不同版本對synchronized關鍵字實現(xiàn)有區(qū)別,所以本文選的是jdk8u40版本,其他版本可能沒有偏向鎖等等邏輯)
首先,讀者明白,使用Synchronized關鍵字時需要一個鎖對象,而底層就是操作這個鎖對象的對象頭,所以我們先從markOop.hpp文件中找到對象頭的描述信息,是不是跟外面8股文描述的一模一樣呢??
對象頭熟悉以后,源碼中就是操作對象頭,不同的鎖狀態(tài)設置不同對象頭,用對象頭來表示不同的鎖狀態(tài),替換對象頭的原子性依靠CAS來保證。如果存在并發(fā),那么CAS競爭失敗的線程就會往下走,一步一步的鎖升級,反而如果沒有競爭那就默認使用偏向鎖。
下面是Hotspot中C++解釋器對于monitorenter字節(jié)碼指令的解釋執(zhí)行源碼(注釋特別詳細)。
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 // 作用:用來記錄鎖狀態(tài) 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) { // 搶坑,為什么這里不需要CAS,因為屬于線程棧(線程變量),線程安全。 entry->set_obj(lockee); int success = false; // 得到epoch的掩碼。 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 // 當前是偏向鎖模式,可以用JVM參數(shù)UseBiasedLocking控制 if (mark->has_bias_pattern()) { uintptr_t thread_ident; uintptr_t anticipated_bias_locking_value; thread_ident = (uintptr_t)istate->thread(); // lockee->klass()->prototype_header() 是否拿到對象的類模板的頭部信息。 // lockee->klass()->prototype_header() | thread_ident) 是類模板頭部信息組合上線程id // mark 是當前鎖對象的頭部信息。 // markOopDesc::age_mask_in_place 是當前對象的年齡信息。 // 所以與年齡無關 // 所以拿鎖對象的原型對象的對象頭控制 // lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark 如果為0 代表當前對象頭偏向鎖偏向了當前線程 anticipated_bias_locking_value = (((uintptr_t)lockee->klass()->prototype_header() | thread_ident) ^ (uintptr_t)mark) & ~((uintptr_t) markOopDesc::age_mask_in_place); // 等于0代表當前鎖對象頭部和類模板頭部一樣。 // 所以這是一次偏向鎖的命中。 if (anticipated_bias_locking_value == 0) { // already biased towards this thread, nothing to do if (PrintBiasedLockingStatistics) { (* BiasedLocking::biased_lock_entry_count_addr())++; } success = true; } // 當前對象頭已經膨脹成輕量級或者重量級鎖了。也即非偏向鎖。 else if ((anticipated_bias_locking_value & markOopDesc::biased_lock_mask_in_place) != 0) { // try revoke bias // 嘗試撤銷偏向鎖 markOop header = lockee->klass()->prototype_header(); if (hash != markOopDesc::no_hash) { header = header->copy_set_hash(hash); } // CAS嘗試取消偏向 if (Atomic::cmpxchg_ptr(header, lockee->mark_addr(), mark) == mark) { if (PrintBiasedLockingStatistics) (*BiasedLocking::revoked_lock_entry_count_addr())++; } } // 來到這里可能表示當前偏向于其他線程。 // 而epoch發(fā)生了變動,表示批量撤銷偏向鎖了。 // 當前線程可以嘗試爭搶一次偏向鎖,沒有成功就去鎖升級 else if ((anticipated_bias_locking_value & epoch_mask_in_place) !=0) { // try rebias // 嘗試重偏向 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競爭,重偏向。 if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), mark) == mark) { if (PrintBiasedLockingStatistics) (* BiasedLocking::rebiased_lock_entry_count_addr())++; } // CAS失敗,鎖升級 else { // 鎖升級邏輯 CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception); } success = true; } // 來到這里表示,當前是匿名偏向鎖(也即暫時還沒有線程占用) // 或者是已經偏向了某個線程,所以這里CAS嘗試一次 else { // try to bias towards thread in case object is anonymously biased 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); // debugging hint DEBUG_ONLY(entry->lock()->set_displaced_header((markOop) (uintptr_t) 0xdeaddead);) // 如果是匿名偏向,這個CAS就有可能成功 // 如果是已經偏向其他線程,這個CAS不能成功,直接往鎖升級走 if (Atomic::cmpxchg_ptr((void*)new_header, lockee->mark_addr(), header) == header) { if (PrintBiasedLockingStatistics) (* BiasedLocking::anonymously_biased_lock_entry_count_addr())++; } // cas失敗 else { // 鎖升級邏輯 CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception); } success = true; } } // traditional lightweight locking // case1:如果當前已經鎖升級了 // case2:如果當前不支持偏向鎖 if (!success) { markOop displaced = lockee->mark()->set_unlocked(); entry->lock()->set_displaced_header(displaced); bool call_vm = UseHeavyMonitors; // UseHeavyMonitors是JVM參數(shù),是否直接開啟重量級鎖 // 如果不直接開啟,就CAS競爭輕量級鎖,競爭成功就直接返回 if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) { // Is it simple recursive case? // CAS失敗可能是鎖重入,如果不是鎖重入,那么就是競爭失敗要往鎖升級邏輯走了。 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 } }
要明白偏向鎖對應的對象頭的幾個部分的意義,然后帶入到源碼中就比較容易理解。
- 線程對象:偏向于那個線程(當沒有線程對象時,就代表是匿名偏向,此時線程都可以去競爭)
- epoch:是否發(fā)生了批量鎖撤銷(為什么要鎖撤銷?因為偏向鎖升級為輕量級鎖就需要撤銷)
- 偏向鎖標志位:0表示無鎖,1表示偏向鎖(偏向鎖和無鎖的鎖標志位都是01)
- 鎖標志位:表示不同鎖狀態(tài),偏向鎖表示為01(要注意無鎖也是表示為01,所以需要額外的偏向鎖標志位來區(qū)分是無鎖還是偏向鎖)
總結
可能源碼部分一直是一個難點,操作的內容太多了,并且還是C++實現(xiàn)的。但是從對象頭的角度去分析理解還是很有幫助。
到此這篇關于Java Synchronized的偏向鎖詳細分析的文章就介紹到這了,更多相關Java Synchronized偏向鎖內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
如何解決SpringBoot集成百度UEditor圖片上傳后直接訪問404
在本篇文章里小編給大家整理的是一篇關于如何解決SpringBoot集成百度UEditor圖片上傳后直接訪問404相關文章,需要的朋友們學習下。2019-11-11在win10系統(tǒng)下,如何配置Spring Cloud alibaba Seata以及出現(xiàn)問題時怎么解決
今天教大家如何在win10系統(tǒng)下,配置Spring Cloud alibaba Seata以及出現(xiàn)問題時怎么解決,文中有非常詳細的介紹及代碼示例,需要的朋友可以參考下2021-06-06IDEA2021.2配置docker如何將springboot項目打成鏡像一鍵發(fā)布部署
這篇文章主要介紹了IDEA2021.2配置docker如何將springboot項目打成鏡像一鍵發(fā)布部署,本文圖文實例相結合給大家介紹的非常詳細,需要的朋友可以參考下2021-09-09解決Java壓縮zip異常java.util.zip.ZipException:duplicate entry
這篇文章主要介紹了解決Java壓縮zip異常java.util.zip.ZipException:duplicate entry:問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-12-12