Java高併發之synchronized關鍵字

席潤發表於2019-02-21

慕課網課程《Java高併發之魂:synchronized深度解析》學習筆記

Synchronized的作用

官方解釋

同步方法支援一種簡單的策略來防止執行緒干擾和記憶體一致性錯誤:如果一個物件對多個執行緒所見,則對該物件變數的所有讀取或寫入都是通過同步方法完成的。

通俗解釋

能夠保證在***同一時刻***最多隻有***一個***執行緒執行該段程式碼,以達到保證併發安全的效果。

分析

一段程式碼被Synchronized所修飾,被修飾的這段程式碼就會以原子的方式執行,多個執行緒在執行這段程式碼的時候不會相互干擾影響,因為多個執行緒之間並不會同時執行這段程式碼,所以不會出現併發問題。如何做到不同時執行?如何知道已經有一個執行緒在執行,那我就不執行呢?

它們會有一把鎖,這把鎖在第一個執行緒去執行的時候被拿到,拿到後這個執行緒就獨佔這把鎖,直到這個執行緒結束或一定條件之後,它才會釋放這把鎖。在這把鎖釋放之前,其他執行緒只能等待。

Synchronized是Java的關鍵字,被Java語言原生支援;是最基本的互斥同步手段;是併發程式設計中的元老級角色,是併發程式設計的必學內容。可以保證程式碼的原子性和可見性。

不使用併發手段會有什麼後果?

程式碼實戰:兩個執行緒同時a++,最後結果會比預計的少。

原因:

count++,它看上去只是一個操作,實際上包含了三個動作:

1、讀取count 2、將count加一 3、將count的值寫入到記憶體中

執行緒不安全

Synchronized的兩個用法

物件鎖

包括***方法鎖***(預設鎖物件為this當前例項物件)和***同步程式碼塊鎖***(自己指定鎖物件)

方法鎖形式:Synchronized修飾普通方法,鎖物件預設為this

程式碼塊形式:手動指定鎖物件,可指定自己建立的物件或this

類鎖

值Synchronized修飾***靜態***的方法或指定鎖為***Class物件***

概念(重要):Java類可能有很多個物件,但***只有一個Class物件***

本質:所謂的類鎖,不過是Class物件的鎖而已。

分析:不同的例項,即不同的執行緒去訪問類鎖的時候,它們獲取到的鎖其實是Class物件,由於Class物件只有一個,所以不同執行緒(無論有哪一個物件例項過來)都只能獲取這唯一的一個鎖,類鎖實際是一個概念性的東西,用來幫助我們理解例項方法和靜態方法的區別。

由於類鎖只有一個,所以不同例項之間會互斥,在同一時刻只能一個例項去訪問被類鎖鎖住的方法。

用法和效果:類鎖只能在同一時刻被一個物件擁有。

類鎖的特殊之處在於,我們即便是不同的Runnable例項,執行緒所對應的類鎖依然只有一個。

形式1:synchronized加在static方法上

形式2:synchronized(*.class)程式碼塊

多執行緒訪問同步方法的7種情況(面試常考)

1、兩個執行緒同時訪問一個物件的同步方法

Thread-0先執行,執行結束後,Thread-1再執行,即他們一個一個的執行,因為他們既是同一個例項,所爭搶的也是同一把鎖,同一時刻只能一個持有,必須相互等待。

2、兩個執行緒訪問的是兩個物件的同步方法

兩個執行緒幾乎同時執行,同時結束,兩者互不干擾,原因為他們所採用的鎖物件不是同一個

3、兩個執行緒訪問的是synchronized的靜態方法

他們會一個一個的執行,鎖生效。

4、同時訪問同步方法與非同步方法(指被synchronized修飾和不被synchronized修飾的方法)

兩個執行緒幾乎同時執行,同時結束,原因為synchronized關鍵字只能作用於被指定的一個方法,非同步方法不受影響。

5、訪問同一個物件的不同普通同步方法

他們會一個一個的執行,synchronized關鍵字雖然沒有明確指定所要物件,其本質原理指定了this物件作為他的鎖,對同一個例項來講,兩個方法拿到的this一樣,兩個方法序列執行。

6、同時訪問靜態synchronized非靜態synchronized方法

兩個執行緒幾乎同時執行,同時結束。原因為synchronized修飾static方法,鎖住的物件是*.Class;Synchronized修飾普通方法,鎖物件預設為物件實本身this,兩個物件不一樣,所以兩者互不干擾。

7、方法拋異常後,是否會釋放鎖

會釋放鎖。丟擲異常後鎖由JVM釋放。

總結

1、一把鎖只能同時被一個執行緒獲取,沒有拿到鎖的執行緒必須等待(對應第1、5種情況);

2、每個例項都對應有自己的一把鎖,不同例項之間互不影響;例外:鎖物件是*.class以及synchronized修飾的是static方法的時候,所有物件共用同一把鎖(對應第22、3、4、6種情況);

3、無論是方法正常執行完畢或者方法丟擲異常,都會釋放鎖(對應第7種情況)

synchronized的性質

可重入

可重入(遞迴鎖):指的是同一執行緒的外層函式獲得鎖之後,內層函式可以直接再次獲取該鎖

好處:避免死鎖、提升封裝性

粒度:預設加鎖範圍是執行緒而非呼叫(用3種情況來說明和pthread的區別)

情況1:證明同一個方法是可重入的 √

情況2:證明可重入不要求是同一個方法 √

情況3:證明可重入不要求是同一個類中的 √

不可中斷

一旦這個鎖已經被別人獲得了,如果我還想獲得,我只能選擇等待或者阻塞,直到別的執行緒釋放這個鎖。如果別人永遠不釋放鎖,那麼我只能永遠地等下去。

(Lock類,擁有中斷的能力,有權中斷現在已經獲取到的鎖的執行緒的執行,也可以退出。)

原理

加鎖和釋放鎖的原理

現象:每一個類的例項對應一把鎖,每一個synchronized的方法都必須首先獲得呼叫該方法類的例項的鎖,方能執行,否則執行緒阻塞;而方法一旦執行,它就獨佔這把鎖,直到該方法返回或丟擲異常,才將鎖釋放。

時間:獲取和釋放鎖的時機:內建鎖

等價程式碼

深入JVM看位元組碼

概況:synchronized使用的鎖是在Java物件頭裡的一個欄位,即Java物件頭裡有一個欄位表示這個物件是否被鎖住。

當執行緒訪問一個同步程式碼塊的時候它必須得到這把鎖,退出整個程式碼塊或丟擲異常必須釋放這把鎖。進入鎖和釋放鎖是基於monitor物件來實現同步方法和同步程式碼塊的。monitor最重要的兩個指令是monitorenter,這個指令會插入到同步程式碼塊開始的位置,與monitorexit ,這個指令會插入到方法結束的時候和退出的時候。enter必須有exit對應,可能多個exit與一個enter對應。

可重入原理:加鎖次數計數器

JVM負責跟蹤物件被加鎖的次數

執行緒第一次給物件加鎖的時候,計數變為1.每當這個相同的執行緒在此物件上再次獲得鎖時,計數會遞增。

每當任務離開時,計數遞減,當計數為0的時候,鎖被完全釋放。

保證可見性的原理:Java記憶體模型

一旦一個方法或程式碼塊被synchronized所修飾,那麼它在執行完畢之後,被鎖住的物件所做的任何修改都要在釋放鎖之前從執行緒記憶體寫回到主記憶體中。在未獲得的程式碼塊或方法得到鎖之後,被鎖定的資料直接由主記憶體讀取。

synchronized的缺陷

效率低:鎖的釋放情況少、試圖獲得鎖時不能設定超時、不能中斷一個正在試圖獲得鎖的執行緒。

不夠靈活(讀寫鎖更靈活:加鎖和釋放的時機單一,每個鎖僅有單一的條件(某個物件),可能是不夠的。

無法知道是否成功獲取到鎖。

常見面試問題

1、synchronized使用注意點:鎖物件不能為空、作用域不宜過大、避免死鎖

2、如何選擇Lock和synchronized關鍵字?

建議:如果可以的話,既不要使用Lock,也不要使用synchronized關鍵字,而是使用java.util.concurrent的包的各種類。如果在程式中synchronized關鍵字適用,那就優先使用這個關鍵字,因為這樣可以減少我們所需要編寫的程式碼,也就減少了出錯的機率。如果特別需要用的Lock、condition獨有的特性時,才使用它們。

3、多執行緒訪問同步方法的各種具體情況,見上

思考題

1、在多個執行緒等待同一個synchronized鎖的時候,JVM如何選擇下一個獲取鎖的執行緒?

競爭這把鎖的又已經在等待中的執行緒、剛進入synchronized關鍵字所包裹程式碼塊,處於Runnable狀的執行緒。哪個執行緒將獲取這把鎖由JVM決定,與JVM的版本和具體實現都有關係,不能依賴演算法,只能判定它是一個隨及、不可控的。

2、synchronized使得同時只有一個執行緒可以執行,效能較差,有什麼辦法可以提升效能?

優化使用範圍,使臨界區在符合要求的情況下儘可能的小。

使用其他型別的lock,synchronized 使用的鎖經過 jdk 版本的升級,效能已經大幅提升了,但相對於更加輕量級的鎖(如讀寫鎖)還是偏重一點,所以可以選擇更合適的鎖。

……

3、我想更靈活地控制鎖的獲取和釋放(現在釋放鎖的時機都被規定死了),怎麼辦?

根據需要自己實現Lock介面

4、什麼是鎖的升級、降級?什麼是JVM裡的偏斜鎖、輕量級鎖、重量級鎖?

可參考

一句話介紹synchronized

JVM會自動通過monitor來加鎖和解鎖,保證了同時只有一個執行緒可以執行指定程式碼,從而保證了執行緒安全,同時具有可重入和不可中斷的性質。

例項程式碼地址

相關文章