騰訊---synchronize關鍵字在虛擬機執行原理是什麼,能談一談什麼是內存可見性,鎖升級嗎

面試官: synchronize關鍵字在虛擬機執行原理是什麼,能談一談什麼是內存可見性,鎖升級嗎

心理分析:面試官一定是想深入考你併發的內容,看你究竟有沒有做過併發處理,大多數開發者在開發App時

往往會忽略調併發處理 ,這道題會難住絕大多數人。

**求職者:**應該存 鎖的執行原理,鎖優化 ,和java對象頭說起

鎖的內存語義

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

  • **內存可見性:**同步快的可見性是由“如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前需要重新執行load或assign操作初始化變量的值”、“對一個變量執行unlock操作之前,必須先把此變量同步回主內存中(執行store和write操作)”這兩條規則獲得的。
  • **操作原子性:**持有同一個鎖的兩個同步塊只能串行地進入

鎖的內存語義:

  • 當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中
  • 當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效。從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量

鎖釋放和鎖獲取的內存語義:

  • 線程A釋放一個鎖,實質上是線程A向接下來將要獲取這個鎖的某個線程發出了(線程A對共享變量所做修改的)消息。
  • 線程B獲取一個鎖,實質上是線程B接收了之前某個線程發出的(在釋放這個鎖之前對共享變量所做修改的)消息。
  • 線程A釋放鎖,隨後線程B獲取這個鎖,這個過程實質上是線程A通過主內存向線程B發送消息

img

synchronized鎖

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

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

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

注意兩點:

1、synchronized同步快對同一條線程來說是可重入的,不會出現自己把自己鎖死的問題;

2、同步塊在已進入的線程執行完之前,會阻塞後面其他線程的進入。

Mutex Lock

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

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

mutex的工作方式:

img

    1. 申請mutex
    1. 如果成功,則持有該mutex
    1. 如果失敗,則進行spin自旋. spin的過程就是在線等待mutex, 不斷髮起mutex gets, 直到獲得mutex或者達到spin_count限制爲止
    1. 依據工作模式的不同選擇yiled還是sleep
    1. 若達到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爲例:

img

鎖優化

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

Synchronized是通過對象內部的一個叫做監視器鎖(monitor)來實現的,監視器鎖本質又是依賴於底層的操作系統的Mutex Lock(互斥鎖)來實現的。而操作系統實現線程之間的切換需要從用戶態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是爲什麼Synchronized效率低的原因。因此,這種依賴於操作系統Mutex Lock所實現的鎖我們稱之爲“重量級鎖”。

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

偏向鎖

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,那麼程序默認會進入輕量級鎖狀態。

輕量級鎖

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

輕量級鎖的加鎖過程:

  • (1)在代碼進入同步塊的時候,如果同步對象鎖狀態爲無鎖狀態(鎖標誌位爲“01”狀態,是否爲偏向鎖爲“0”),虛擬機首先將在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之爲 Displaced Mark Word。這時候線程堆棧與對象頭的狀態如下圖所示。

img

  • (2)拷貝對象頭中的Mark Word複製到鎖記錄中。
  • (3)拷貝成功後,虛擬機將使用CAS操作嘗試將對象的Mark Word更新爲指向Lock Record的指針,並將Lock record裏的owner指針指向object mark word。如果更新成功,則執行步驟(3),否則執行步驟(4)。
  • (4)如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位設置爲“00”,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如下圖所示。

img

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

輕量級鎖的解鎖過程:

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

重量級鎖

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

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

(具體見前面的mutex lock)

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

 

 

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

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

其他鎖優化

鎖消除

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

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

看下面這段程序:

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的引用沒有傳遞到該方法外,不可能被其他線程拿到該引用),所以其實這過程是線程安全的,可以將鎖消除。

鎖粗化

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

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

舉個例子:

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

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

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

自旋鎖與自適應自旋鎖

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

總結

  • **synchronized特點:**保證內存可見性、操作原子性

  • synchronized影響性能的原因

    • 1、加鎖解鎖操作需要額外操作;
    • 2、互斥同步對性能最大的影響是阻塞的實現,因爲阻塞涉及到的掛起線程和恢復線程的操作都需要轉入內核態中完成(用戶態與內核態的切換的性能代價是比較大的)
  • 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線程與操作系統內核態線程一一映射,所以涉及到用戶態和內核態的切換、操作系統內核態中的線程的阻塞和恢復。

扣扣掃碼加入粉絲裙,可領取福利;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章