10.關於synchronized的一切,我都寫在這裡了

2023-01-13 12:00:29

大家好,我是王有志。關注王有志,一起聊技術,聊遊戲,從北漂生活談到國際風雲。

之前我們已經通過3篇文章由淺到深的分析了synchronized的用法和原理:

還有一篇是關於並行控制中常用鎖的設計《一文看懂並行程式設計中的鎖》。可以說是從設計,到用法,再到實現原理,對synchronized進行了全方位的剖析。

今天我們就用之前學習的內容解答一些熱點題目。全量題解可以猛戳此處或者文末的閱讀原文。

Tips:標題是「抄襲」《一年一度喜劇大賽》作品《夢幻麗莎髮廊》的臺詞。由仁科,茂濤,蔣龍,蔣詩萌和歐劍宇表演,爆笑推薦。

synchronized基礎篇

基礎篇的問題主要集中在synchronized的用法上。例如:

  1. synchronized.class物件,代表著什麼?

  2. synchronized什麼情況下是物件鎖?什麼情況下是類鎖?

  3. 如果物件的多個方法新增了synchronized,那麼物件有幾把鎖?

很多小夥伴解答這類問題時喜歡背諸如「synchronized修飾靜態方法,作用的範圍是整個靜態方法,作用物件是這個類的所有物件」這種,相當於直接背結論,忽略了原理。

先來回顧下《synchronized都問啥?》中提到的原理:Java中每個物件都與一個監視器關聯。synchronized鎖定與物件關聯的監視器(可以理解為鎖定物件本身),鎖定成功後才可以繼續執行

舉個例子:

public class Human {
	public static synchronized void run() {
		// 業務邏輯
	}
}

synchronized修飾靜態方法,而靜態方法是類所有,可以理解為synchronized鎖定了Human.class物件,接下來我們推導現象。

假設執行緒t1執行run方法且尚未結束,即t1鎖定了Human.class,且尚未釋放,那麼此時所有試圖鎖定Human.class的執行緒都會被阻塞。

例如,執行緒t2執行run方法會被阻塞:

Thread t2 = new Thread(Human::run);  
t2.start();

如果我們新增如下方法呢?

public synchronized void eat() {  
    // 業務邏輯  
}

synchronized修飾實體方法,屬於物件所有,可以理解為synchronized鎖定了當前物件

執行以下測試程式碼,會發生阻塞嗎?

new Thread(Human::run, "t1")).start();
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
	Human human = new Human();
	human.eat();  
}, "t2")).start();

答案是不會,因為t1鎖定的是Human.class物件,而t2鎖定的是Human的範例物件,它們之間不存在任何競爭。

再新增一個方法,並執行如下測試,會發生阻塞嗎?

public static synchronized void walk() {
	// 業務邏輯
}

public static void main(String[] args) throws InterruptedException {
	new Thread(Human::run, "t1").start();
	TimeUnit.SECONDS.sleep(1);
	new Thread(Human::walk, "t2").start();  
}

答案是執行緒t2會阻塞,因為執行緒t1和執行緒t2在競爭同一個Human.class物件,而很明顯執行緒t1會搶先鎖定Human.class物件。

最後再做一個測試,新增如下方法和測試程式碼:

public synchronized void drink() {
	// 業務邏輯
}

public static void main(String[] args) throws InterruptedException {
	Human human = new Human();  
	
	new Thread(human::eat, "t1").start();
	TimeUnit.SECONDS.sleep(1);
	new Thread(human::drink, "t2").start();
	
	new Thread(()-> {
        Human t3 = new Human();
        t3.eat();
    }, "t3").start();
    TimeUnit.SECONDS.sleep(1);
    
    new Thread(()-> {
        Human t4 = new Human();
        t4.eat();
    }, "t4").start();
}

小夥伴們可以按照用法結合原理的方式,推導這段程式碼的執行結果。

Tips:業務邏輯可以執行TimeUnit.SECONDS.sleep(60)模擬長期持有。

synchronized進階篇

進階篇則主要考察synchronized的原理,例如:

  • synchronized是如何保證原子性,有序性和可見性的?

  • 詳細描述synchronized的原理和鎖升級的過程。

  • 為什麼說synchronized是悲觀鎖/非公平鎖/可重入鎖?

synchronized的並行保證

假設有如下程式碼:

private static int count = 0;
  
public static synchronized void add() {
	......
    count++;
    ......
}

在正確同步的前提下,同一時間有且僅有一個執行緒能夠執行add方法,對count進行修改。

此時便「營造」了一種單執行緒環境,而編譯器對重排序做出了「as-if-serial」的保證,因此不會存在有序性問題。同樣的,僅有一個執行緒執行count++,那麼也不存在原子性問題

至於可見性,我們在《什麼是synchronized的重量級鎖》中釋放重量級鎖的部分看到了storeload記憶體屏障,該屏障保證了寫操作的資料對下一讀操作可見。

Tips

  • synchronized並沒有禁止重排序,而是「營造」了單執行緒環境;

  • 記憶體屏障我們在volatile中重點解釋。

synchronized的實現原理

synchronized是JVM根據管程的設計思想實現的互斥鎖synchronized修飾程式碼塊時,編譯後會新增monitorentermonitorexit指令,修飾方法時,會新增ACC_SYNCHRONIZED存取標識。

Java 1.6之後,synchronized的內部結構實際上分為偏向鎖,輕量級鎖和重量級鎖3部分。

當執行緒進入synchronized方法後,且未發生競爭,會修改物件頭中偏向的執行緒ID,此時synchronized處於偏向鎖狀態。

當產生輕微競爭後(常見於執行緒交替執行),會升級(膨脹)到輕量級鎖的狀態。

當產生激烈競爭後,輕量級鎖會升級(膨脹)到重量級鎖,此時只有一個執行緒可以獲取到物件的監視器,其餘執行緒會被park(暫停)且進入等待佇列,等待喚醒。

synchronized的特性實現

為什麼說synchronized是悲觀鎖?來回顧下《一文看懂並行程式設計中的鎖》中提到的悲觀鎖,悲觀鎖認為並行存取共用總是會發生修改,因此在進入臨界區前一定會執行加鎖操作

那麼對於synchronized來說,無論是偏向鎖,輕量級鎖還是重量級鎖,使用synchronized總是會發生加鎖,因此是悲觀鎖。

為什麼說synchronized是非公平鎖?接著回顧下非公平鎖,非公平性體現在發生阻塞後的喚醒並不是按照先來後到的順序進行的

synchronized中,預設策略是將cxq佇列中的資料移入到EntryList後再進行喚醒,並沒有按照先後順序執行。實際上我們也不知道cxqEntryList中的執行緒到底誰先進入等待的。

為什麼說synchronized是可重入鎖?回顧下可重入鎖,可重入指的是允許同一個執行緒反覆多次加鎖

使用上,synchronized允許同一個執行緒多次進入。底層實現上,synchronized內部維護了計數器_recursions,發生重入時,計數器+1,退出時計數器-1。

通過_recursions的命名,我們也能知道Java中的可重入鎖就是POSIX中的遞迴鎖。

結語

本文的內容比較簡單,主要是根據之前的內容回答一些熱點問題。不說是做到學以致用,至少做到學習後,能回答一些面試問題。

當然更深層次的意義,在於指導我們合理的使用synchronized以及我們可以從中借鑑到的設計思想。


好了,今天就到這裡了,Bye~~