騰訊面試必問:不能逃避的synchronize關鍵字。

本專欄專一分享大型Bat面試知識,後續會持續更新,喜歡的話麻煩點擊一個關注java

面試官:
synchronize關鍵字在虛擬機執行原理是什麼,能談一談什麼是內存可見性,鎖升級嗎
心理分析
面試官必定是想深刻考你併發的內容,看你究竟有沒有作過併發處理,大多數開發者在開發App時每每會忽略調併發處理 ,這道題會難住絕大多數人。
求職者:
應該存 鎖的執行原理,鎖優化 ,和java對象頭提及

一.鎖的內存語義

synchronized的底層是使用操做系統的mutex lock實現的。git

  • 內存可見性:同步快的可見性是由「若是對一個變量執行lock操做,將會清空工做內存中此變量的值,在執行引擎使用這個變量前須要從新執行load或assign操做初始化變量的值」、「對一個變量執行unlock操做以前,必須先把此變量同步回主內存中(執行store和write操做)」這兩條規則得到的。
  • 操做原子性:持有同一個鎖的兩個同步塊只能串行地進入
鎖的內存語義:
  • 當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中
  • 當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效。從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量
鎖釋放和鎖獲取的內存語義:
  • 線程A釋放一個鎖,實質上是線程A向接下來將要獲取這個鎖的某個線程發出了(線程A對共享變量所作修改的)消息。
  • 線程B獲取一個鎖,實質上是線程B接收了以前某個線程發出的(在釋放這個鎖以前對共享變量所作修改的)消息。
  • 線程A釋放鎖,隨後線程B獲取這個鎖,這個過程實質上是線程A經過主內存向線程B發送消息

二.synchronized鎖

synchronized用的鎖是存在Java對象頭裏的。程序員

JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步。代碼塊同步是使用monitorenter和monitorexit指令實現的,monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處。任何對象都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態。github

根據虛擬機規範的要求,在執行monitorenter指令時,首先要去嘗試獲取對象的鎖,若是這個對象沒被鎖定,或者當前線程已經擁有了那個對象的鎖,把鎖的計數器加1;相應地,在執行monitorexit指令時會將鎖計數器減1,當計數器被減到0時,鎖就釋放了。若是獲取對象鎖失敗了,那當前線程就要阻塞等待,直到對象鎖被另外一個線程釋放爲止。面試

注意兩點:安全

一、synchronized同步快對同一條線程來講是可重入的,不會出現本身把本身鎖死的問題;多線程

二、同步塊在已進入的線程執行完以前,會阻塞後面其餘線程的進入。併發

三.Mutex Lock

監視器鎖(Monitor)本質是依賴於底層的操做系統的Mutex Lock(互斥鎖)來實現的。每一個對象都對應於一個可稱爲" 互斥鎖" 的標記,這個標記用來保證在任一時刻,只能有一個線程訪問該對象。app

互斥鎖:用於保護臨界區,確保同一時間只有一個線程訪問數據。對共享資源的訪問,先對互斥量進行加鎖,若是互斥量已經上鎖,調用線程會阻塞,直到互斥量被解鎖。在完成了對共享資源的訪問後,要對互斥量進行解鎖。編輯器

mutex的工做方式:

  • 申請mutex
  • 若是成功,則持有該mutex
  • 若是失敗,則進行spin自旋. spin的過程就是在線等待mutex, 不斷髮起mutex gets, 直到得到mutex或者達到spin_count限制爲止
  • 依據工做模式的不一樣選擇yiled仍是sleep
  • 若達到sleep限制或者被主動喚醒或者完成yield, 則重複1)~4)步,直到得到爲止

因爲Java的線程是映射到操做系統的原生線程之上的,若是要阻塞或喚醒一條線程,都須要操做系統來幫忙完成,這就須要從用戶態轉換到核心態中,所以狀態轉換須要耗費不少的處理器時間。因此synchronized是Java語言中的一個重量級操做。在JDK1.6中,虛擬機進行了一些優化,譬如在通知操做系統阻塞線程以前加入一段自旋等待過程,避免頻繁地切入到核心態中:

synchronized與java.util.concurrent包中的ReentrantLock相比,因爲JDK1.6中加入了針對鎖的優化措施(見後面),使得synchronized與ReentrantLock的性能基本持平。ReentrantLock只是提供了synchronized更豐富的功能,而不必定有更優的性能,因此在synchronized能實現需求的狀況下,優先考慮使用synchronized來進行同步。

四.Java對象頭


在運行期間,Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化,以32位的JDK爲例:

五.鎖優化

偏向鎖、輕量級鎖、重量級鎖

Synchronized是經過對象內部的一個叫作監視器鎖(monitor)來實現的,監視器鎖本質又是依賴於底層的操做系統的Mutex Lock(互斥鎖)來實現的。而操做系統實現線程之間的切換須要從用戶態轉換到核心態,這個成本很是高,狀態之間的轉換須要相對比較長的時間,這就是爲何Synchronized效率低的緣由。所以,這種依賴於操做系統Mutex Lock所實現的鎖咱們稱之爲「重量級鎖」。

Java SE 1.6爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」:鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態。鎖能夠升級但不能降級。

5.1 偏向鎖

HotSpot的做者通過研究發現,大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到。偏向鎖是爲了在只有一個線程執行同步塊時提升性能。

當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,之後該線程在進入和退出同步塊時不須要進行CAS操做來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖。引入偏向鎖是爲了在無多線程競爭的狀況下儘可能減小沒必要要的輕量級鎖執行路徑,由於輕量級鎖的獲取及釋放依賴屢次CAS原子指令,而偏向鎖只須要在置換ThreadID的時候依賴一次CAS原子指令(因爲一旦出現多線程競爭的狀況就必須撤銷偏向鎖,因此偏向鎖的撤銷操做的性能損耗必須小於節省下來的CAS原子指令的性能消耗)。

偏向鎖獲取過程:

  • (1)訪問Mark Word中偏向鎖的標識是否設置成1,鎖標誌位是否爲01——確認爲可偏向狀態。
  • (2)若是爲可偏向狀態,則測試線程ID是否指向當前線程,若是是,進入步驟(5),不然進入步驟(3)。
  • (3)若是線程ID並未指向當前線程,則經過CAS操做競爭鎖。若是競爭成功,則將Mark Word中線程ID設置爲當前線程ID,而後執行(5);若是競爭失敗,執行(4)。
  • (4)若是CAS獲取偏向鎖失敗,則表示有競爭(CAS獲取偏向鎖失敗說明至少有過其餘線程曾經得到過偏向鎖,由於線程不會主動去釋放偏向鎖)。當到達全局安全點(safepoint)時,會首先暫停擁有偏向鎖的線程,而後檢查持有偏向鎖的線程是否活着(由於可能持有偏向鎖的線程已經執行完畢,可是該線程並不會主動去釋放偏向鎖),若是線程不處於活動狀態,則將對象頭設置成無鎖狀態(標誌位爲「01」),而後從新偏向新的線程;若是線程仍然活着,撤銷偏向鎖後升級到輕量級鎖狀態(標誌位爲「00」),此時輕量級鎖由原持有偏向鎖的線程持有,繼續執行其同步代碼,而正在競爭的線程會進入自旋等待得到該輕量級鎖。
  • (5)執行同步代碼。

偏向鎖的釋放過程:

如上步驟(4)。偏向鎖使用了一種等到競爭出現才釋放偏向鎖的機制:偏向鎖只有遇到其餘線程嘗試競爭偏向鎖時,持有偏向鎖的線程纔會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷,須要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態,撤銷偏向鎖後恢復到未鎖定(標誌位爲「01」)或輕量級鎖(標誌位爲「00」)的狀態。

關閉偏向鎖:

偏向鎖在Java 6和Java 7裏是默認啓用的。因爲偏向鎖是爲了在只有一個線程執行同步塊時提升性能,若是你肯定應用程序裏全部的鎖一般狀況下處於競爭狀態,能夠經過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,那麼程序默認會進入輕量級鎖狀態。

5.2輕量級鎖

輕量級鎖是爲了在線程近乎交替執行同步塊時提升性能。

輕量級鎖的加鎖過程:

  • (1)在代碼進入同步塊的時候,若是同步對象鎖狀態爲無鎖狀態(鎖標誌位爲「01」狀態,是否爲偏向鎖爲「0」),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之爲 Displaced Mark Word。這時候線程堆棧與對象頭的狀態以下圖所示。
  • (2)拷貝對象頭中的Mark Word複製到鎖記錄中。
  • (3)拷貝成功後,虛擬機將使用CAS操做嘗試將對象的Mark Word更新爲指向Lock Record的指針,並將Lock record裏的owner指針指向object mark word。若是更新成功,則執行步驟(3),不然執行步驟(4)。
  • (4)若是這個更新動做成功了,那麼這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位設置爲「00」,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態以下圖所示。

  • (5)若是這個更新操做失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,若是是就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行。不然說明多個線程競爭鎖,若當前只有一個等待線程,則可經過自旋稍微等待一下,可能另外一個線程很快就會釋放鎖。 可是當自旋超過必定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹爲重量級鎖,重量級鎖使除了擁有鎖的線程之外的線程都阻塞,防止CPU空轉,鎖標誌的狀態值變爲「10」,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。

輕量級鎖的解鎖過程:

  • (1)經過CAS操做嘗試把線程中複製的Displaced Mark Word對象替換當前的Mark Word。
  • (2)若是替換成功,整個同步過程就完成了。
  • (3)若是替換失敗,說明有其餘線程嘗試過獲取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時,喚醒被掛起的線程。
5.3重量級鎖

如上輕量級鎖的加鎖過程步驟(5),輕量級鎖所適應的場景是線程近乎交替執行同步塊的狀況,若是存在同一時間訪問同一鎖的狀況,就會致使輕量級鎖膨脹爲重量級鎖。Mark Word的鎖標記位更新爲10,Mark Word指向互斥量(重量級鎖)

Synchronized的重量級鎖是經過對象內部的一個叫作監視器鎖(monitor)來實現的,監視器鎖本質又是依賴於底層的操做系統的Mutex Lock(互斥鎖)來實現的。而操做系統實現線程之間的切換須要從用戶態轉換到核心態,這個成本很是高,狀態之間的轉換須要相對比較長的時間,這就是爲何Synchronized效率低的緣由。

(具體見前面的mutex lock)

5.4偏向鎖、輕量級鎖、重量級鎖之間轉換



偏向所鎖,輕量級鎖都是樂觀鎖,重量級鎖是悲觀鎖。

  • 一個對象剛開始實例化的時候,沒有任何線程來訪問它的時候。它是可偏向的,意味着,它如今認爲只可能有一個線程來訪問它,因此當第一個線程來訪問它的時候,它會偏向這個線程,此時,對象持有偏向鎖。偏向第一個線程,這個線程在修改對象頭成爲偏向鎖的時候使用CAS操做,並將對象頭中的ThreadID改爲本身的ID,以後再次訪問這個對象時,只須要對比ID,不須要再使用CAS在進行操做。
  • 一旦有第二個線程訪問這個對象,由於偏向鎖不會主動釋放,因此第二個線程能夠看到對象時偏向狀態,這時代表在這個對象上已經存在競爭了。檢查原來持有該對象鎖的線程是否依然存活,若是掛了,則能夠將對象變爲無鎖狀態,而後從新偏向新的線程。若是原來的線程依然存活,則立刻執行那個線程的操做棧,檢查該對象的使用狀況,若是仍然須要持有偏向鎖,則偏向鎖升級爲輕量級鎖,(偏向鎖就是這個時候升級爲輕量級鎖的),此時輕量級鎖由原持有偏向鎖的線程持有,繼續執行其同步代碼,而正在競爭的線程會進入自旋等待得到該輕量級鎖;若是不存在使用了,則能夠將對象回覆成無鎖狀態,而後從新偏向。
  • 輕量級鎖認爲競爭存在,可是競爭的程度很輕,通常兩個線程對於同一個鎖的操做都會錯開,或者說稍微等待一下(自旋),另外一個線程就會釋放鎖。 可是當自旋超過必定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹爲重量級鎖,重量級鎖使除了擁有鎖的線程之外的線程都阻塞,防止CPU空轉。

六其餘鎖優化

6.1鎖消除

鎖消除即刪除沒必要要的加鎖操做。虛擬機即時編輯器在運行時,對一些「代碼上要求同步,可是被檢測到不可能存在共享數據競爭」的鎖進行消除。

根據代碼逃逸技術,若是判斷到一段代碼中,堆上的數據不會逃逸出當前線程,那麼能夠認爲這段代碼是線程安全的,沒必要要加鎖。

看下面這段程序:

public class SynchronizedTest {

    public static void main(String[] args) {
        SynchronizedTest test = new SynchronizedTest();

        for (int i = 0; i < 100000000; i++) {
            test.append("abc", "def");
        }
    }

    public void append(String str1, String str2) {
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }
}

雖然StringBuffer的append是一個同步方法,可是這段程序中的StringBuffer屬於一個局部變量,而且不會從該方法中逃逸出去(即StringBuffer sb的引用沒有傳遞到該方法外,不可能被其餘線程拿到該引用),因此其實這過程是線程安全的,能夠將鎖消除。

6.2鎖粗化

若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,甚至加鎖操做是出如今循環體中的,那即便沒有出現線程競爭,頻繁地進行互斥同步操做也會致使沒必要要的性能損耗。

若是虛擬機檢測到有一串零碎的操做都是對同一對象的加鎖,將會把加鎖同步的範圍擴展(粗化)到整個操做序列的外部。

舉個例子:

public class StringBufferTest {
    StringBuffer stringBuffer = new StringBuffer();

    public void append(){
        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("c");
    }
}

這裏每次調用stringBuffer.append方法都須要加鎖和解鎖,若是虛擬機檢測到有一系列連串的對同一個對象加鎖和解鎖操做,就會將其合併成一次範圍更大的加鎖和解鎖操做,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖。

6.3自旋鎖與自適應自旋鎖

  • 引入自旋鎖的緣由:互斥同步對性能最大的影響是阻塞的實現,由於掛起線程和恢復線程的操做都須要轉入內核態中完成,這些操做給系統的併發性能帶來很大的壓力。同時虛擬機的開發團隊也注意到在許多應用上面,共享數據的鎖定狀態只會持續很短一段時間,爲了這一段很短的時間頻繁地阻塞和喚醒線程是很是不值得的。
  • 自旋鎖:讓該線程執行一段無心義的忙循環(自旋)等待一段時間,不會被當即掛起(自旋不放棄處理器額執行時間),看持有鎖的線程是否會很快釋放鎖。自旋鎖在JDK 1.4.2中引入,默認關閉,可是可使用-XX:+UseSpinning開開啓;在JDK1.6中默認開啓。
  • 自旋鎖的缺點:自旋等待不能替代阻塞,雖然它能夠避免線程切換帶來的開銷,可是它佔用了處理器的時間。若是持有鎖的線程很快就釋放了鎖,那麼自旋的效率就很是好;反之,自旋的線程就會白白消耗掉處理器的資源,它不會作任何有意義的工做,這樣反而會帶來性能上的浪費。因此說,自旋等待的時間(自旋的次數)必需要有一個限度,例如讓其循環10次,若是自旋超過了定義的時間仍然沒有獲取到鎖,則應該被掛起(進入阻塞狀態)。經過參數-XX:PreBlockSpin能夠調整自旋次數,默認的自旋次數爲10。
  • 自適應的自旋鎖:JDK1.6引入自適應的自旋鎖,自適應就意味着自旋的次數再也不是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定:若是在同一個鎖的對象上,自旋等待剛剛成功得到過鎖,而且持有鎖的線程正在運行中,那麼虛擬機就會認爲此次自旋也頗有可能再次成功,進而它將容許自旋等待持續相對更長的時間。若是對於某個鎖,自旋不多成功得到過,那在之後要獲取這個鎖時將可能省略掉自旋過程,以免浪費處理器資源。簡單來講,就是線程若是自旋成功了,則下次自旋的次數會更多,若是自旋失敗了,則自旋的次數就會減小。
  • 自旋鎖使用場景:從輕量級鎖獲取的流程中咱們知道,當線程在獲取輕量級鎖的過程當中執行CAS操做失敗時,是要經過自旋來獲取重量級鎖的。(見前面「輕量級鎖」)

七.總結

  • synchronized特色:保證內存可見性、操做原子性
  • synchronized影響性能的緣由:

    • 一、加鎖解鎖操做須要額外操做;

二、互斥同步對性能最大的影響是阻塞的實現,由於阻塞涉及到的掛起線程和恢復線程的操做都須要轉入內核態中完成(用戶態與內核態的切換的性能代價是比較大的)

  • synchronized鎖:對象頭中的Mark Word根據鎖標誌位的不一樣而被複用

    • 偏向鎖:在只有一個線程執行同步塊時提升性能。Mark Word存儲鎖偏向的線程ID,之後該線程在進入和退出同步塊時不須要進行CAS操做來加鎖和解鎖,只需簡單比較ThreadID。特色:只有等到線程競爭出現才釋放偏向鎖,持有偏向鎖的線程不會主動釋放偏向鎖。以後的線程競爭偏向鎖,會先檢查持有偏向鎖的線程是否存活,若是不存貨,則對象變爲無鎖狀態,從新偏向;若是仍存活,則偏向鎖升級爲輕量級鎖,此時輕量級鎖由原持有偏向鎖的線程持有,繼續執行其同步代碼,而正在競爭的線程會進入自旋等待得到該輕量級鎖
    • 輕量級鎖:在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,嘗試拷貝鎖對象目前的Mark Word到棧幀的Lock Record,若拷貝成功:虛擬機將使用CAS操做嘗試將對象的Mark Word更新爲指向Lock Record的指針,並將Lock record裏的owner指針指向對象的Mark Word。若拷貝失敗:若當前只有一個等待線程,則可經過自旋稍微等待一下,可能持有輕量級鎖的線程很快就會釋放鎖。 可是當自旋超過必定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖膨脹爲重量級鎖
    • 重量級鎖:指向互斥量(mutex),底層經過操做系統的mutex lock實現。等待鎖的線程會被阻塞,因爲Linux下Java線程與操做系統內核態線程一一映射,因此涉及到用戶態和內核態的切換、操做系統內核態中的線程的阻塞和恢復。

關於我

更多Android高級面試合集放在github上面了

須要的小夥伴能夠點擊關於我 聯繫我獲取

很是但願和你們一塊兒交流 , 共同進步

也能夠掃一掃, 目前是一名程序員,不只分享 Android開發相關知識,同時還分享技術人成長曆程,包括我的總結,職場經驗,面試經驗等,但願能讓你少走一點彎路。

相關文章
相關標籤/搜索