《安琪拉與面試官二三事》系列文章
一個HashMap能跟面試官扯上半個小時
一個synchronized跟面試官扯了半個小時java
《安琪拉教魯班學算法》系列文章c++
話說上回HashMap跟面試官扯了半個小時以後,二面迎來了沒有削弱前的鐘馗,法師的鉤子讓安琪拉有點絕望。鍾馗穿着有些微微泛黃的格子道袍,站在安琪拉對面,開始發難,其中讓安琪拉印象很是深入的是法師的synchronized 鉤子。程序員
面試官: 你先自我介紹一下吧!github
安琪拉: 我是安琪拉,草叢三婊之一,最強中單(鍾馗冷哼)!哦,不對,串場了,我是**,目前在--公司作--系統開發。面試
面試官: 剛纔聽一面的同事說大家上次聊到了synchronized,你藉口說要回去補籃,如今能跟我講講了吧?算法
安琪拉: 【上來就丟鉤子,都不寒暄幾句,問我吃沒吃】嗯嗯,是有聊到 synchronized。後端
面試官: 那你跟我說說爲何會須要synchronized?什麼場景下使用synchronized?數組
安琪拉: 這個就要說到多線程訪問共享資源了,當一個資源有可能被多個線程同時訪問並修改的話,須要用到鎖,仍是畫個圖給您看一下,請看👇圖:安全
安琪拉: 如上圖所示,好比在王者榮耀程序中,咱們隊有二個線程分別統計後裔和安琪拉的經濟,A線程從內存中read 當前隊伍總經濟加載到線程的本地棧,進行 +100 操做以後,這時候B線程也從內存中取出經濟值 + 200,將200寫回內存,B線程剛執行完,後腳A線程將100 寫回到內存中,就出問題了,咱們隊的經濟應該是300, 可是內存中存的倒是100,你說糟不糟心。
面試官: 那你跟我講講用 synchronized 怎麼解決這個問題的?
安琪拉: 在訪問競態資源時加鎖,由於多個線程會修改經濟值,所以經濟值就是競態資源,給您show 一下吧?下圖是不加鎖的代碼以及控制檯的輸出,請您過目:
二個線程,A線程讓隊伍經濟 +1 ,B線程讓經濟 + 2,分別執行一千次,正確的結果應該是3000,結果獲得的倒是 2845。
安琪拉: 👇這個就是加鎖以後的代碼和控制檯的輸出。
面試官: 我看你👆用synchronized 鎖住的是代碼塊,synchronized 還有別的做用範圍嗎?
安琪拉: 嗯嗯,synchronized 有如下三種做用範圍:
在靜態方法上加鎖;
在非靜態方法上加鎖;
在代碼塊上加鎖;
示例代碼以下
public class SynchronizedSample { private final Object lock = new Object(); private static int money = 0; //非靜態方法 public synchronized void noStaticMethod(){ money++; } //靜態方法 public static synchronized void staticMethod(){ money++; } public void codeBlock(){ //代碼塊 synchronized (lock){ money++; } } }
面試官: 那你瞭解 synchronized 這三種做用範圍的加鎖方式的區別嗎?
安琪拉: 瞭解。首先要明確一點:鎖是加在對象上面的,咱們是在對象上加鎖。
重要事情說三遍:在對象上加鎖 ✖️ 3 (這也是爲何wait / notify 須要在鎖定對象後執行,只有先拿到鎖才能釋放鎖)
這三種做用範圍的區別實際是被加鎖的對象的區別,請看下錶:
做用範圍 | 鎖對象 |
---|---|
非靜態方法 | 當前對象 => this |
靜態方法 | 類對象 => SynchronizedSample.class (一切皆對象,這個是類對象) |
代碼塊 | 指定對象 => lock (以上面的代碼爲例) |
面試官: 那你清楚 JVM 是怎麼經過synchronized 在對象上實現加鎖,保證多線程訪問競態資源安全的嗎?
安琪拉: 【天啦擼, 該來的仍是要來】(⊙o⊙)…額,這個提及來有點複雜,我怕時間不夠,要不下次再約?
面試官: 別下次了,今天我有的是時間,你慢慢講,我慢慢👂你說。
安琪拉: 那要跟您好好說道了。分二個時間段來跟您討論,先說到盤古開天闢地,女媧造石補天,咳咳,很差意思扯遠了。。。。。。
面試官: 那你分別跟我講講JDK 6 之前 synchronized爲何這麼重? JDK6 以後的偏向鎖和輕量級鎖是怎麼回事?
安琪拉: 好的。首先要了解 synchronized 的實現原理,須要理解二個預備知識:
第一個預備知識:須要知道 Java 對象頭,鎖的類型和狀態和對象頭的Mark Word息息相關;
synchronized 鎖 和 對象頭息息相關。咱們來看下對象的結構:
對象存儲在堆中,主要分爲三部份內容,對象頭、對象實例數據和對齊填充(數組對象多一個區域:記錄數組長度),下面簡單說一下三部份內容,雖然 synchronized 只與對象頭中的 Mard Word相關。
對象頭:
對象頭分爲二個部分,Mard Word 和 Klass Word,👇列出了詳細說明:
對象頭結構 | 存儲信息-說明 |
---|---|
Mard Word | 存儲對象的hashCode、鎖信息或分代年齡或GC標誌等信息 |
Klass Word | 存儲指向對象所屬類(元數據)的指針,JVM經過這個肯定這個對象屬於哪一個類 |
對象實例數據:
如上圖所示,類中的 成員變量data 就屬於對象實例數據;
對齊填充:
JVM要求對象佔用的空間必須是8 的倍數,方便內存分配(以字節爲最小單位分配),所以這部分就是用於填滿不夠的空間湊數用的。
第二個預備知識:須要瞭解 Monitor ,每一個對象都有一個與之關聯的Monitor 對象;Monitor對象屬性以下所示( Hospot 1.7 代碼) 。
//👇圖詳細介紹重要變量的做用 ObjectMonitor() { _header = NULL; _count = 0; // 重入次數 _waiters = 0, // 等待線程數 _recursions = 0; _object = NULL; _owner = NULL; // 當前持有鎖的線程 _WaitSet = NULL; // 調用了 wait 方法的線程被阻塞 放置在這裏 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 等待鎖 處於block的線程 有資格成爲候選資源的線程 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
對象關聯的 ObjectMonitor 對象有一個線程內部競爭鎖的機制,以下圖所示:
面試官: 預備的二個知識我大致看了,後面給我講講 JDK 6 之前 synchronized具體實現邏輯吧。
安琪拉: 好的。【開始個人表演】
當有二個線程A、線程B都要開始給咱們隊的經濟 money變量 + 錢,要進行操做的時候 ,發現方法上加了synchronized鎖,這時線程調度到A線程執行,A線程就搶先拿到了鎖。拿到鎖的步驟爲:
- 1.1 將 MonitorObject
中的 _owner設置成 A線程;
- 1.2 將 mark word 設置爲 Monitor 對象地址,鎖標誌位改成10;
- 1.3 將B 線程阻塞放到 ContentionList 隊列;
JVM 每次從Waiting Queue 的尾部取出一個線程放到OnDeck做爲候選者,可是若是併發比較高,Waiting Queue會被大量線程執行CAS操做,爲了下降對尾部元素的競爭,將Waiting Queue 拆分紅ContentionList 和 EntryList 二個隊列, JVM將一部分線程移到EntryList 做爲準備進OnDeck的預備線程。另外說明幾點:
全部請求鎖的線程首先被放在ContentionList這個競爭隊列中;
Contention List 中那些有資格成爲候選資源的線程被移動到 Entry List 中;
任意時刻,最多隻有一個線程正在競爭鎖資源,該線程被成爲 OnDeck;
當前已經獲取到所資源的線程被稱爲 Owner;
處於 ContentionList、EntryList、WaitSet 中的線程都處於阻塞狀態,該阻塞是由操做系統來完成的(Linux 內核下采用 pthread_mutex_lock
內核函數實現的);
做爲Owner 的A 線程執行過程當中,可能調用wait 釋放鎖,這個時候A線程進入 Wait Set , 等待被喚醒。
以上就是我想說的 synchronized 在 JDK 6以前的實現原理。
面試官: 那你知道 synchronized 是公平鎖仍是非公平鎖嗎?
安琪拉: 非公平的。主要有如下二點緣由:
面試官: 你前面說到 JDK 6 以後synchronized 作了優化,跟我講講?
安琪拉: 不要着急! 容我點個治療,再跟你掰扯掰扯。前面說了鎖跟對象頭的 Mark Word 密切相關,咱們把目光放到對象頭的 Mark Word
上, Mark Word
存儲結構以下圖和源代碼註釋(以32位JVM爲例,後面的討論都基於32位JVM的背景,64位會特殊說明)。
Mard Word
會在不一樣的鎖狀態下,32位指定區域都有不一樣的含義,這個是爲了節省存儲空間,用4 字節就表達了完整的狀態信息,固然,對象某一時刻只會是下面5 種狀態種的某一種。
下面是簡化後的 Mark Word
hash: 保存對象的哈希碼 age: 保存對象的分代年齡 biased_lock: 偏向鎖標識位 lock: 鎖狀態標識位 JavaThread*: 保存持有偏向鎖的線程ID epoch: 保存偏向時間戳
安琪拉: 因爲 synchronized 重量級鎖有如下二個問題, 所以JDK 6 以後作了改進,引入了偏向鎖和輕量級鎖:
依賴底層操做系統的 mutex
相關指令實現,加鎖解鎖須要在用戶態和內核態之間切換,性能損耗很是明顯。
研究人員發現,大多數對象的加鎖和解鎖都是在特定的線程中完成。也就是出現線程競爭鎖的狀況機率比較低。他們作了一個實驗,找了一些典型的軟件,測試同一個線程加鎖解鎖的重複率,以下圖所示,能夠看到重複加鎖比例很是高。早期JVM 有 19% 的執行時間浪費在鎖上。
Thin locks are a lot cheaper than inflated locks, but their performance suffers from the fact that every compare-and-swap operation must be executed atomically on multi-processor machines, although most objects are locked and unlocked only by one particular thread.
It was reported that 19% of the total execution time was wasted by thread synchronization in an early version of Java virtual machine。
面試官: 你跟我講講 JDK 6 以來 synchronized 鎖狀態怎麼從無鎖狀態到偏向鎖的嗎?
安琪拉: OK的啦!,咱們來看下圖對象從無鎖到偏向鎖轉化的過程(JVM -XX:+UseBiasedLocking 開啓偏向鎖):
Mark Word
當中;Mark Word
在這個過程當中的轉化Mark Word
拷貝到線程棧的 Lock Record中,這個位置叫 displayced hdr,以下圖所示:面試官: 看來對synchronized 頗有研究嘛。我鍾馗不信難不倒你,那輕量級鎖何時會升級爲重量級鎖, 請回答?
安琪拉: 當鎖升級爲輕量級鎖以後,若是依然有新線程過來競爭鎖,首先新線程會自旋嘗試獲取鎖,嘗試到必定次數(默認10次)依然沒有拿到,鎖就會升級成重量級鎖。
面試官: 爲何這麼設計?
安琪拉: 通常來講,同步代碼塊內的代碼應該很快就執行結束,這時候線程B 自旋一段時間是很容易拿到鎖的,可是若是不巧,沒拿到,自旋其實就是死循環,很耗CPU的,所以就直接轉成重量級鎖咯,這樣就不用了線程一直自旋了。
這就是鎖膨脹的過程,下圖是Mark Word 和鎖狀態的轉化圖
主要👆圖我標註出來的,鎖當前爲可偏向狀態,偏向鎖狀態位置就是1,看到不少網上的文章都寫錯了,把這裏寫成只有鎖發生偏向纔會置爲1,必定要注意。
面試官: 既然偏向鎖有撤銷,還會膨脹,性能損耗這麼大,還須要用他們呢?
安琪拉: 若是肯定競態資源會被高併發的訪問,建議經過-XX:-UseBiasedLocking
參數關閉偏向鎖,偏向鎖的好處是併發度很低的狀況下,同一個線程獲取鎖不須要內存拷貝的操做,免去了輕量級鎖的在線程棧中建Lock Record,拷貝Mark Down的內容,也免了重量級鎖的底層操做系統用戶態到內核態的切換,由於前面說了,須要使用系統指令。另外Hotspot 也作了另外一項優化,基於鎖對象的epoch 批量偏向和批量撤銷偏向,這樣能夠大大下降了單次偏向鎖的CAS和鎖撤銷帶來的損耗,👇圖是研究人員作的壓測:
安琪拉: 他們在幾款典型軟件上作了測試,發現基於epoch 批量撤銷偏向鎖和批量加偏向鎖能大幅提高吞吐量,可是併發量特別大的時候性能就沒有什麼特別大的提高了。
面試官:能夠能夠,那你看過synchronized 底層實現源碼沒有?
安琪拉: 那固然啦,源碼是個人二技能,高爆發的傷害能不能打出來就看它了,咱們一步一步來。
咱們把文章開頭的示例代碼編譯成class 文件,而後經過javap -v SynchronizedSample.class
來看下synchronized 到底在源碼層面如何實現的?
以下圖所示:
安琪拉: synchronized 在代碼塊上是經過 monitorenter 和 monitorexit指令實現,在靜態方法和 方法上加鎖是在方法的flags 中加入 ACC_SYNCHRONIZED 。JVM 運行方法時檢查方法的flags,遇到同步標識開始啓動前面的加鎖流程,在方法內部遇到monitorenter指令開始加鎖。
monitorenter 指令函數源代碼在 InterpreterRuntime::monitorenter
中
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem)) #ifdef ASSERT thread->last_frame().interpreter_frame_verify_monitor(elem); #endif 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) { // 嘗試偏向鎖 ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK); } else { // 輕量鎖邏輯 ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK); } assert(Universe::heap()->is_in_reserved_or_null(elem->obj()), "must be NULL or an object"); #ifdef ASSERT thread->last_frame().interpreter_frame_verify_monitor(elem); #endif IRT_END
偏向鎖代碼
// ----------------------------------------------------------------------------- // Fast Monitor Enter/Exit // This the fast monitor enter. The interpreter and compiler use // some assembly copies of this code. Make sure update those code // if the following function is changed. The implementation is // extremely sensitive to race condition. Be careful. 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) ; }
偏向鎖的實現具體代碼在 BiasedLocking::revoke_and_rebias
中,由於函數很是長,就不貼出來,有興趣的能夠在Hotspot 1.8-biasedLocking.cpp去看。
輕量級鎖代碼流程
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { //獲取對象的markOop數據mark markOop mark = obj->mark(); assert(!mark->has_bias_pattern(), "should not see bias pattern here"); //判斷mark是否爲無鎖狀態 & 不可偏向(鎖標識爲01,偏向鎖標誌位爲0) if (mark->is_neutral()) { // Anticipate successful CAS -- the ST of the displaced mark must // be visible <= the ST performed by the CAS. // 保存Mark 到 線程棧 Lock Record 的displaced_header中 lock->set_displaced_header(mark); // CAS 將 Mark Down 更新爲 指向 lock 對象的指針,成功則獲取到鎖 if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { TEVENT (slow_enter: release stacklock) ; return ; } // Fall through to inflate() ... } else // 根據對象mark 判斷已經有鎖 & mark 中指針指的當前線程的Lock Record(當前線程已經獲取到了,沒必要重試獲取) 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; } lock->set_displaced_header(markOopDesc::unused_mark()); // 鎖膨脹 ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
作個假設,如今線程A 和B 同時執行到臨界區if (mark->is_neutral()):
一、線程A和B都把Mark Word複製到各自的_displaced_header字段,該數據保存在線程的棧幀上,是線程私有的;
二、Atomic::cmpxchg_ptr 屬於原子操做,保障了只有一個線程能夠把Mark Word中替換成指向本身線程棧 displaced_header中的,假設A線程執行成功,至關於A獲取到了鎖,開始繼續執行同步代碼塊;
三、線程B執行失敗,退出臨界區,經過ObjectSynchronizer::inflate方法開始膨脹鎖;
面試官: synchronized 源碼這部分能夠了,👂不下去了。你跟我講講Java中除了synchronized 還有別的鎖嗎?
安琪拉: 還有ReentrantLock也能夠實現加鎖。
面試官: 那寫段代碼實現以前加經濟的一樣效果。
安琪拉: coding 如👇圖:
面試官: 哦,那你跟我說說ReentrantLock 的底層實現原理?
安琪拉: 天色已晚,咱們能改日再聊嗎?
面試官: 那你回去等通知吧。
安琪拉: 【心裏是崩潰的】,看來此次面試就黃了,😔,心累。
未完,下一篇介紹ReentrantLock相關的底層原理,看安琪拉如何大戰鍾馗面試官三百回合。
補充說明:
在代碼中查看對象頭信息方法:
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> </dependency>
public static void main(String[] args) { Test obj = new Test(); ClassLayout layout = ClassLayout.parseInstance(obj); //打印空對象大小 System.out.println(layout.instanceSize()); System.out.println(layout.toPrintable()); synchronized (obj){ System.out.println("after lock"); System.out.println(layout.toPrintable()); } System.out.println("after re-lock"); System.out.println(layout.toPrintable()); }
控制檯輸出以下:
這個是反着的,這裏是高地址位表示低位數據,低地址位表示高位數據, 👆能夠看出對象後三位是001,0表明不可偏向狀態,01表明無鎖狀態,第二個000,表示輕量級鎖狀態,最後釋放鎖,變回01狀態無鎖狀態。
關注Wx公衆號:【安琪拉的博客】 —揭祕Java後端技術,還原技術背後的本質
《安琪拉與面試官二三事》系列文章 持續更新中
一個HashMap能跟面試官扯上半個小時
一個synchronized跟面試官扯了半個小時