大家好,我是王有志。關註王有志,一起聊技術,聊游戲,從北漂生活談到國際風雲。 之前我們已經通過3篇文章由淺到深的分析了synchronized的用法和原理: synchronized的基礎:synchronized都問啥? 偏向鎖升級到輕量級鎖:從源碼揭秘偏向鎖的升級 輕量級鎖升級到重量級鎖:什麼是 ...
大家好,我是王有志。關註王有志,一起聊技術,聊游戲,從北漂生活談到國際風雲。
之前我們已經通過3篇文章由淺到深的分析了synchronized
的用法和原理:
-
synchronized
的基礎:synchronized都問啥? -
偏向鎖升級到輕量級鎖:從源碼揭秘偏向鎖的升級
-
輕量級鎖升級到重量級鎖:什麼是synchronized的重量級鎖
還有一篇是關於併發控制中常用鎖的設計《一文看懂併發編程中的鎖》。可以說是從設計,到用法,再到實現原理,對synchronized進行了全方位的剖析。
今天我們就用之前學習的內容解答一些熱點題目。全量題解可以猛戳此處或者文末的閱讀原文。
Tips:標題是“抄襲”《一年一度喜劇大賽》作品《夢幻麗莎髮廊》的臺詞。由仁科,茂濤,蔣龍,蔣詩萌和歐劍宇表演,爆笑推薦。
synchronized基礎篇
基礎篇的問題主要集中在synchronized
的用法上。例如:
-
synchronized
鎖.class
對象,代表著什麼? -
synchronized
什麼情況下是對象鎖?什麼情況下是類鎖? -
如果對象的多個方法添加了
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
修飾代碼塊時,編譯後會添加monitorenter
和monitorexit
指令,修飾方法時,會添加ACC_SYNCHRONIZED
訪問標識。
Java 1.6之後,synchronized
的內部結構實際上分為偏向鎖,輕量級鎖和重量級鎖3部分。
當線程進入synchronized
方法後,且未發生競爭,會修改對象頭中偏向的線程ID,此時synchronized
處於偏向鎖狀態。
當產生輕微競爭後(常見於線程交替執行),會升級(膨脹)到輕量級鎖的狀態。
當產生激烈競爭後,輕量級鎖會升級(膨脹)到重量級鎖,此時只有一個線程可以獲取到對象的監視器,其餘線程會被park(暫停)且進入等待隊列,等待喚醒。
synchronized的特性實現
為什麼說synchronized
是悲觀鎖?來回顧下《一文看懂併發編程中的鎖》中提到的悲觀鎖,悲觀鎖認為併發訪問共用總是會發生修改,因此在進入臨界區前一定會執行加鎖操作。
那麼對於synchronized
來說,無論是偏向鎖,輕量級鎖還是重量級鎖,使用synchronized
總是會發生加鎖,因此是悲觀鎖。
為什麼說synchronized
是非公平鎖?接著回顧下非公平鎖,非公平性體現在發生阻塞後的喚醒並不是按照先來後到的順序進行的。
在synchronized
中,預設策略是將cxq
隊列中的數據移入到EntryList
後再進行喚醒,並沒有按照先後順序執行。實際上我們也不知道cxq
和EntryList
中的線程到底誰先進入等待的。
為什麼說synchronized
是可重入鎖?回顧下可重入鎖,可重入指的是允許同一個線程反覆多次加鎖。
使用上,synchronized
允許同一個線程多次進入。底層實現上,synchronized
內部維護了計數器_recursions
,發生重入時,計數器+1,退出時計數器-1。
通過_recursions
的命名,我們也能知道Java中的可重入鎖就是POSIX中的遞歸鎖。
結語
本文的內容比較簡單,主要是根據之前的內容回答一些熱點問題。不說是做到學以致用,至少做到學習後,能回答一些面試問題。
當然更深層次的意義,在於指導我們合理的使用synchronized
以及我們可以從中借鑒到的設計思想。
好了,今天就到這裡了,Bye~~