Synchronized詳解

三分惡發表於2021-02-10

synchronized是Java多執行緒中元老級的鎖,也是面試的高頻考點,讓我們來詳細瞭解synchronized吧。

在Java中,synchronized鎖可能是我們最早接觸的鎖了,在 JDK1.5之前synchronized是一個重量級鎖,相對於juc包中的Lock,synchronized顯得比較笨重

慶幸的是在 Java 6 之後 Java 官⽅對從 JVM 層⾯對synchronized進行⼤優化,所以現在的 synchronized 鎖效率也優化得很不錯。

一、synchronized 使用

1、synchronized的作用

synchronized 的作用主要有三:

  • (1)、原子性所謂原子性就是指一個操作或者多個操作,要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。synchronized修飾的類或物件的所有操作都是原子的,因為在執行操作之前必須先獲得類或物件的鎖,直到執行完才能釋放。
  • (2)、可見性:**可見性是指多個執行緒訪問一個資源時,該資源的狀態、值資訊等對於其他執行緒都是可見的。 **synchronized和volatile都具有可見性,其中synchronized對一個類或物件加鎖時,一個執行緒如果要訪問該類或物件必須先獲得它的鎖,而這個鎖的狀態對於其他任何執行緒都是可見的,並且在釋放鎖之前會將對變數的修改重新整理到共享記憶體當中,保證資源變數的可見性。
  • (3)、有序性有序性值程式執行的順序按照程式碼先後執行。 synchronized和volatile都具有有序性,Java允許編譯器和處理器對指令進行重排,但是指令重排並不會影響單執行緒的順序,它影響的是多執行緒併發執行的順序性。synchronized保證了每個時刻都只有一個執行緒訪問同步程式碼塊,也就確定了執行緒執行同步程式碼塊是分先後順序的,保證了有序性。

2、synchronized的使用

Synchronized主要有三種用法

  • (1)、修飾例項方法: 作用於當前物件例項加鎖,進入同步程式碼前要獲得 當前物件例項的鎖

    synchronized void method() {
      //業務程式碼
    }
    
  • (2)、修飾靜態方法: 也就是給當前類加鎖,會作用於類的所有物件例項 ,進入同步程式碼前要獲得 當前 class 的鎖。因為靜態成員不屬於任何一個例項物件,是類成員( static 表明這是該類的一個靜態資源,不管 new 了多少個物件,只有一份)。所以,如果一個執行緒 A 呼叫一個例項物件的非靜態 synchronized 方法,而執行緒 B 需要呼叫這個例項物件所屬類的靜態 synchronized 方法,是允許的,不會發生互斥現象,因為訪問靜態 synchronized 方法佔用的鎖是當前類的鎖,而訪問非靜態 synchronized 方法佔用的鎖是當前例項物件鎖

synchronized void staic method() {
  //業務程式碼
}
  • (3)、修飾程式碼塊 :指定加鎖物件,對給定物件/類加鎖。synchronized(this|object) 表示進入同步程式碼庫前要獲得給定物件的鎖synchronized(類.class) 表示進入同步程式碼前要獲得 當前 class 的鎖
synchronized(this) {
  //業務程式碼
}

簡單總結一下

synchronized 關鍵字加到 static 靜態方法和 synchronized(class) 程式碼塊上都是是給 Class 類上鎖。

synchronized 關鍵字加到例項方法上是給物件例項上鎖。

接下來看一個 synchronized 使用經典例項—— 執行緒安全的單例模式:

public class Singleton {
    //保證有序性,防止指令重排
    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }

    public  static Singleton getUniqueInstance() {
       //先判斷物件是否已經例項過,沒有例項化過才進入加鎖程式碼
        if (uniqueInstance == null) {
            //類物件加鎖
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

二、synchronized同步原理

資料同步需要依賴鎖,那鎖的同步又依賴誰?synchronized給出的答案是在軟體層面依賴JVM,而j.u.c.Lock給出的答案是在硬體層面依賴特殊的CPU指令。

1、synchronized 同步語句塊原理

public class SynchronizedDemo {
	public void method() {
		synchronized (this) {
			System.out.println("synchronized 程式碼塊");
		}
	}
}

通過 JDK 自帶的 javap 命令檢視 SynchronizedDemo 類的相關位元組碼資訊:首先切換到類的對應目錄執行 javac SynchronizedDemo.java 命令生成編譯後的 .class 檔案,然後執行javap -c -s -v -l SynchronizedDemo.class

image-20210210160804090


從圖中可以看出:

synchronized 同步語句塊的實現使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步程式碼塊的開始位置, monitorexit 指令則指明同步程式碼塊的結束位置。**

當執行 monitorenter 指令時,執行緒試圖獲取鎖也就是獲取 物件監視器 monitor 的持有權。

在 Java 虛擬機器(HotSpot)中,Monitor 是基於 C++實現的,由ObjectMonitor實現的。每個物件中都內建了一個 ObjectMonitor物件。

另外,wait/notify等方法也依賴於monitor物件,這就是為什麼只有在同步的塊或者方法中才能呼叫wait/notify等方法,否則會丟擲java.lang.IllegalMonitorStateException的異常的原因。

在執行monitorenter時,會嘗試獲取物件的鎖,如果鎖的計數器為 0 則表示鎖可以被獲取,獲取後將鎖計數器設為 1 也就是加 1。

在執行 monitorexit 指令後,將鎖計數器設為 0,表明鎖被釋放。如果獲取物件鎖失敗,那當前執行緒就要阻塞等待,直到鎖被另外一個執行緒釋放為止。

2、synchronized 修飾方法原理

public class SynchronizedDemo2 {
	public synchronized void method() {
		System.out.println("synchronized 方法");
	}
}

反編譯一下:

image-20210210161841561

synchronized 修飾的方法並沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC_SYNCHRONIZED 標識,該標識指明瞭該方法是一個同步方法。JVM 通過該 ACC_SYNCHRONIZED 訪問標誌來辨別一個方法是否宣告為同步方法,從而執行相應的同步呼叫。

簡單總結一下

synchronized 同步語句塊的實現使用的是 monitorentermonitorexit 指令,其中 monitorenter 指令指向同步程式碼塊的開始位置,monitorexit 指令則指明同步程式碼塊的結束位置。

synchronized 修飾的方法並沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC_SYNCHRONIZED 標識,該標識指明瞭該方法是一個同步方法。

不過兩者的本質都是對物件監視器 monitor 的獲取。

三、synchronized同步概念

1、Java物件頭

在JVM中,物件在記憶體中的佈局分為三塊區域:物件頭、例項資料和對齊填充

img

synchronized用的鎖是存在Java物件頭裡的。

Hotspot 有兩種物件頭:

  • 陣列型別,如果物件是陣列型別,則虛擬機器用3個字寬 (Word)儲存物件頭
  • 非陣列型別:如果物件是非陣列型別,則用2字寬儲存物件頭。

物件頭由兩部分組成

  • Mark Word:儲存自身的執行時資料,例如 HashCode、GC 年齡、鎖相關資訊等內容。
  • Klass Pointer:型別指標指向它的類後設資料的指標。

64 位虛擬機器 Mark Word 是 64bit,在執行期間,Mark Word裡儲存的資料會隨著鎖標誌位的變化而變化。

img

2、監視器(Monitor)

任何一個物件都有一個Monitor與之關聯,當且一個Monitor被持有後,它將處於鎖定狀態。Synchronized在JVM裡的實現都是 基於進入和退出Monitor物件來實現方法同步和程式碼塊同步,雖然具體實現細節不一樣,但是都可以通過成對的MonitorEnter和MonitorExit指令來實現。

  1. MonitorEnter指令:插入在同步程式碼塊的開始位置,當程式碼執行到該指令時,將會嘗試獲取該物件Monitor的所有權,即嘗試獲得該物件的鎖;
  2. MonitorExit指令:插入在方法結束處和異常處,JVM保證每個MonitorEnter必須有對應的MonitorExit;

那什麼是Monitor?可以把它理解為 一個同步工具,也可以描述為 一種同步機制,它通常被 描述為一個物件。

與一切皆物件一樣,所有的Java物件是天生的Monitor,每一個Java物件都有成為Monitor的潛質,因為在Java的設計中 ,每一個Java物件自打孃胎裡出來就帶了一把看不見的鎖,它叫做內部鎖或者Monitor鎖

也就是通常說Synchronized的物件鎖,MarkWord鎖標識位為10,其中指標指向的是Monitor物件的起始地址。在Java虛擬機器(HotSpot)中,Monitor是由ObjectMonitor實現的。

四、synchronized優化

從JDK5引入了現代作業系統新增加的CAS原子操作( JDK5中並沒有對synchronized關鍵字做優化,而是體現在J.U.C中,所以在該版本concurrent包有更好的效能 ),從JDK6開始,就對synchronized的實現機制進行了較大調整,包括使用JDK5引進的CAS自旋之外,還增加了自適應的CAS自旋、鎖消除、鎖粗化、偏向鎖、輕量級鎖這些優化策略。由於此關鍵字的優化使得效能極大提高,同時語義清晰、操作簡單、無需手動關閉,所以推薦在允許的情況下儘量使用此關鍵字,同時在效能上此關鍵字還有優化的空間。

鎖主要存在四種狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖。但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級

img

1、偏向鎖

偏向鎖是JDK6中的重要引進,因為HotSpot作者經過研究實踐發現,在大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低,引進了偏向鎖。

當一個執行緒訪問同步塊並獲取鎖時,會在物件頭和棧幀中的鎖記錄裡儲存鎖偏向的執行緒ID,以後該執行緒在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下物件頭的Mark Word裡是否儲存著指向當前執行緒的偏向鎖。

如果測試成功,表示執行緒已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設定成1(表示當前是偏向鎖):如果沒有設定,則使用CAS競爭鎖;如果設定了,則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒。

偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他執行緒嘗試競爭偏向鎖時, 持有偏向鎖的執行緒才會釋放鎖。

偏向鎖的撤銷,需要等待全域性安全點(在這個時間點上沒有正在執行的位元組碼)。它會首先暫停擁有偏向鎖的執行緒,然後檢查持有偏向鎖的執行緒是否活著, 如果執行緒不處於活動狀態,則將物件頭設定成無鎖狀態;如果執行緒仍然活著,擁有偏向鎖的棧會被執行,遍歷偏向物件的鎖記錄,棧中的鎖記錄和物件頭的Mark Word要麼重新偏向於其他執行緒,要麼恢復到無鎖或者標記物件不適合作為偏向鎖,最後喚醒暫停的執行緒。

下圖中的線 程1演示了偏向鎖初始化的流程,執行緒2演示了偏向鎖撤銷的流程:

img

2、輕量級鎖

引入輕量級鎖的主要目的是 在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用作業系統互斥量產生的效能消耗。當關閉偏向鎖功能或者多個執行緒競爭偏向鎖導致偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖。

(1)輕量級鎖加鎖

執行緒在執行同步塊之前,JVM會先在當前執行緒的棧楨中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中,官方稱為Displaced Mark Word。然後執行緒嘗試使用 CAS將物件頭中的Mark Word替換為指向鎖記錄的指標。如果成功,當前執行緒獲得鎖,如果失敗,表示其他執行緒競爭鎖,當前執行緒便嘗試使用自旋來獲取鎖。

(2)輕量級鎖解鎖

輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到物件頭,如果成 功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。

下圖是 兩個執行緒同時爭奪鎖,導致鎖膨脹的流程圖:

img

因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的執行緒被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處於這個狀態下,其他執行緒試圖獲取鎖時, 都會被阻塞住,當持有鎖的執行緒釋放鎖之後會喚醒這些執行緒,被喚醒的執行緒就會進行新一輪的奪鎖之爭。

3、鎖的優缺點比較

各種鎖並不是相互代替的,而是在不同場景下的不同選擇,絕對不是說重量級鎖就是不合適的。每種鎖是隻能升級,不能降級,即由偏向鎖->輕量級鎖->重量級鎖,而這個過程就是開銷逐漸加大的過程。

如果是單執行緒使用,那偏向鎖毫無疑問代價最小,並且它就能解決問題,連CAS都不用做,僅僅在記憶體中比較下物件頭就可以了;

如果出現了其他執行緒競爭,則偏向鎖就會升級為輕量級鎖;

如果其他執行緒通過一定次數的CAS嘗試沒有成功,則進入重量級鎖;

鎖的優缺點的對比如下表:

優點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法僅有奈米級的差距 如果執行緒間存在鎖的競爭,會帶來額外的鎖撤銷的消耗 適用於只有一個執行緒訪問的同步塊場景
輕量級鎖 競爭的執行緒不會阻塞,提高了程式的相應速度 如果始終得不到鎖競爭的執行緒,使用自旋會消耗CPU 追求響應時間
同步響應非常快
重量級鎖 執行緒競爭不使用自旋,不會消耗CPU 執行緒阻塞,響應時間緩慢 追求吞吐量
同步塊執行速度較長


參考:

【1】:2020最新Java併發進階常見面試題總結.md

【2】:方騰飛等編著 《Java併發程式設計的藝術》

【3】:synchronized 實現原理

【4】:深入分析Synchronized原理(阿里面試題)

【5】:☆啃碎併發(七):深入分析Synchronized原理

【6】:深入理解synchronized底層原理,一篇文章就夠了!

相關文章