- Synchronized可以作用在哪里? 分別通過對象鎖和類鎖進行舉例。
- Synchronized本質上是通過什么保證線程安全的? 分三個方面回答:加鎖和釋放鎖的原理,可重入原理,保證可見性原理。
- Synchronized由什么樣的缺陷? Java Lock是怎么彌補這些缺陷的。
- Synchronized和Lock的對比,和選擇?
- Synchronized在使用時有何注意事項?
- Synchronized修飾的方法在拋出異常時,會釋放鎖嗎?
- 多個線程等待同一個snchronized鎖的時候,JVM如何選擇下一個獲取鎖的線程?
- Synchronized使得同時只有一個線程可以執行,性能比較差,有什么提升的方法?
- 我想更加靈活地控制鎖的釋放和獲取(現在釋放鎖和獲取鎖的時機都被規定死了),怎么辦?
- 什么是鎖的升級和降級? 什么是JVM里的偏斜鎖、輕量級鎖、重量級鎖?
- 不同的JDK中對Synchronized有何優化?
Synchronized用法
注意,synchronized 內置鎖 是一種 對象鎖(鎖的是對象而非引用變量),作用粒度是對象 ,可以用來實現對 臨界資源的同步互斥訪問 ,是 可重入 的。其可重入最大的作用是避免死鎖
synchronized只能用於代碼塊或者方法,不可以用於類。如果要使用類鎖,可以將類對象設為xxx.class或者修飾靜態代碼塊。
一個對象有一把鎖(class也是對象),且同時只能被一個線程獲得。其他未獲得鎖的線程只能等待
每個實例對象都有一把鎖(this),且不同實例對象的鎖互不影響,除非:鎖對象是*.class以及synchronized修飾的是static方法的時候,所有對象公用同一把鎖
(也就是類鎖)
synchroniezd修飾的方法,正常執行結束和拋出異常都會釋放鎖。
Synchronized的用法可以分為兩種:對象鎖和類鎖
對象鎖,有兩種形式:代碼塊&&方法鎖
代碼塊形式下,可以由用戶自己指定 作為鎖的對象 也可以 使用this。
方法鎖,即使用synchronized關鍵字修飾方法名,該情況下默認使用this作為鎖對象
類鎖:synchronize修飾靜態的方法
或指定鎖對象為Class對象
實現原理:
java中每個對象都關聯着一個監視器鎖(monitor),每一個對象在同一時間只與一個monitor(鎖)相關聯。
和monitor有關的命令有兩個
monitorenter:用於當前線程獲取monitor
Monitorexit:用於當前線程釋放monitor
Monitorenter:
而一個monitor在同一時間只能被一個線程獲得,一個線程在嘗試獲得與這個對象相關聯的Monitor鎖的所有權的時候,monitorenter指令會發生如下3中情況之一:
- 當該monitor計數器count = 0時,意味着該鎖還沒有被線程獲得,那么這個線程會立即取得monitor,並使計數器+1.+1后別的線程再想獲取,就需要等待。
- 如果同一線程獲取鎖后,想要再次重入,那么monitor count會再次+1,並隨着重入次數的增多,count也不斷增加。
- 如果這個monitor 已經被其他線程獲取了,那么等待釋放。
Monitorexit:
釋放對於monitor的所有權,釋放過程很簡單,就是將monitor的計數器減1,如果減完以后,計數器不是0,則代表剛才是重入進來的,當前線程還繼續持有這把鎖的所有權,如果計數器變成0,則代表當前線程不再擁有該monitor的所有權,即釋放鎖。

如上圖所示,一個線程通過1號門進入Entry Set(入口區),如果在入口區沒有線程等待,那么這個線程就會獲取監視器成為監視器的Owner,然后執行監視區域的代碼。如果在入口區中有其它線程在等待,那么新來的線程也會和這些線程一起等待。線程在持有監視器的過程中,有兩個選擇,一個是正常執行監視器區域的代碼,釋放監視器,通過5號門退出監視器;還有可能等待某個條件的出現,於是它會通過3號門到Wait Set(等待區)休息,直到相應的條件滿足后再通過4號門進入重新獲取監視器再執行。
如果獲取失敗,該線程就進入同步狀態,線程狀態變為BLOCKED,當Object的監視器占有者釋放后,在同步隊列中得線程就會有機會重新獲取該監視器。
JVM中鎖的優化
簡單來說在JVM中monitorenter和monitorexit字節碼依賴於底層的操作系統的Mutex Lock來實現的,但是由於使用Mutex Lock需要將當前線程掛起並從用戶態切換到內核態來執行
,這種切換的代價是非常昂貴的;然而在現實中的大部分情況下,同步方法是運行在單線程環境(無鎖競爭環境)如果每次都調用Mutex Lock那么將嚴重的影響程序的性能。不過在jdk1.6中對鎖的實現引入了大量的優化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)、適應性自旋(Adaptive Spinning)等技術來減少鎖操作的開銷。
鎖粗化(Lock Coarsening)
:也就是減少不必要的緊連在一起的unlock,lock操作,將多個連續的鎖擴展成一個范圍更大的鎖。
鎖消除(Lock Elimination)
:通過運行時JIT編譯器的逃逸分析來消除一些沒有在當前同步塊以外被其他線程共享的數據的鎖保護,通過逃逸分析也可以在線程本地Stack上進行對象空間的分配(同時還可以減少Heap上的垃圾收集開銷)。
輕量級鎖(Lightweight Locking)
:這種鎖實現的背后基於這樣一種假設,即在真實的情況下我們程序中的大部分同步代碼一般都處於無鎖競爭狀態(即單線程執行環境),在無鎖競爭的情況下完全可以避免調用操作系統層面的重量級互斥鎖,取而代之的是在monitorenter和monitorexit中只需要依靠一條CAS原子指令
就可以完成鎖的獲取及釋放。當存在鎖競爭的情況下,執行CAS指令失敗的線程將調用操作系統互斥鎖進入到阻塞狀態,
當鎖被釋放的時候被喚醒(具體處理步驟下面詳細討論)。
偏向鎖(Biased Locking)
:是為了在無鎖競爭的情況下避免在鎖獲取過程中執行不必要的CAS原子指令,因為CAS原子指令雖然相對於重量級鎖來說開銷比較小但還是存在非常可觀的本地延遲。
適應性自旋(Adaptive Spinning)
:當線程在獲取輕量級鎖的過程中執行CAS操作失敗時,在進入與monitor相關聯的操作系統重量級鎖(mutex semaphore)前會進入忙等待(Spinning)然后再次嘗試,當嘗試一定的次數后如果仍然沒有成功則調用與該monitor關聯的semaphore(即互斥鎖)進入到阻塞狀態。
鎖的類型
在Java SE 1.6里Synchronied同步鎖,一共有四種狀態:無鎖
、偏向鎖
、輕量級所
、重量級鎖
,它會隨着競爭情況逐漸升級。鎖可以升級但是不可以降級,目的是為了提供獲取鎖和釋放鎖的效率。
鎖膨脹方向: 無鎖 → 偏向鎖 → 輕量級鎖 → 重量級鎖 (此過程是不可逆的)
自旋鎖
在多進程競爭鎖時,當一個線程獲取到鎖后,會阻塞其他嘗試獲取該所的線程。線程的掛起和恢復
都需要要從用戶態轉入到內核態中完成,這個操作開銷對操作系統的並發性能產生了一定的壓力。
同時大部分的共享數據的鎖定狀態只會持續很短的時間,為了這段時間而去掛起和恢復線程並不值得。完全可以讓另一個沒有獲取到鎖的線程不放棄CPU時間片,不斷的輪詢鎖是否釋放,等待有鎖的線程釋放鎖。
為了讓線程等待,我們需要讓線程執行自旋。
如果鎖占用的時間非常的短,那么自旋鎖的新能會非常的好,相反,其會帶來更多的性能開銷(因為在線程自旋時,始終會占用CPU的時間片,如果鎖占用的時間太長,那么自旋的線程會白白消耗掉CPU資源)。
如果自選超過了限定的次數仍然沒有成功獲取到鎖,就應該使用傳統的方式去掛起線程了,在JDK定義中,自旋鎖默認的自旋次數為10次,用戶可以使用參數-XX:PreBlockSpin
來更改。
可是現在又出現了一個問題:如果線程鎖在線程自旋剛結束就釋放掉了鎖,那么是不是有點得不償失。所以這時候我們需要更加聰明的鎖來實現更加靈活的自旋
。來提高並發的性能。(這里則需要自適應自旋鎖!)
自適應自旋鎖
自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋 時間及鎖的擁有者的狀態來決定的。如果在同一個鎖對象上,自旋等待的線程剛剛成功獲取過鎖,並且持有鎖的線程正在運行中,那么JVM會認為該鎖自旋獲取到鎖的可能性很大,會自動增加等待時間。比如增加到100次循環。相反,如果對於某個鎖,自旋很少成功獲取鎖。那再以后要獲取這個鎖時將可能省略掉自旋過程直接掛起,以避免浪費處理器資源。
鎖消除
在編譯時,對於一些代碼上明確要求同步但是檢測不存在共享數據競爭的鎖進行消除。
消除的依據來自於 JVM會判斷在一段程序中的需要同步的數據 明顯不會逃逸出去 從而被其他線程訪問到,那JVM就把它們當作棧上數據對待,認為這些數據時線程獨有的,不需要加同步。此時就會進行鎖消除。
在Java API中有很多方法都是加了同步的,那么此時JVM會判斷這段代碼是否需要加鎖。如果數據並不會逃逸,則會進行鎖消除。
鎖粗化
原則上,我們都知道在加同步鎖時,盡可能的將同步塊的作用范圍限制到盡量小的范圍(只在共享數據的實際作用域中才進行同步,這樣是為了使得需要同步的操作數量盡可能變小。在存在鎖同步競爭中,也可以使得等待鎖的線程盡早的拿到鎖)。
大部分上述情況是完美正確的,但是如果存在連串的一系列操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作時出現在循環體中的,那即使沒有線程競爭,頻繁地進行互斥同步操作也會導致不必要地性能操作。
這里貼上根據上述Javap 編譯地情況編寫地實例java類
public static String test04(String s1, String s2, String s3) {
StringBuilder sb = new StringBuilder();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
在上述地連續append()操作中就屬於這類情況。JVM會檢測到這樣一連串地操作都是對同一個對象加鎖,那么JVM會將加鎖同步地范圍擴展(粗化)到整個一系列操作的 外部,使整個一連串地append()操作只需要加鎖一次就可以了。
輕量級鎖
需要注意的是輕量級鎖並不是替代重量級鎖的,而是對在大多數情況下同步塊並不會有競爭出現提出的一種優化
。它可以減少重量級鎖對線程的阻塞帶來地線程開銷。從而提高並發性能。
對象頭中(Object Header
)存在兩部分。第一部分用於存儲對象自身的運行時數據,HashCode
、GC Age
、鎖標記位
、是否為偏向鎖
等。一般為32位或者64位(視操作系統位數定)。官方稱之為Mark Word
,它是實現輕量級鎖和偏向鎖的關鍵。
在線程執行同步塊之前,JVM會先在當前線程的棧幀中創建一個名為鎖記錄(Lock Record
)的空間,用於存儲鎖對象目前的Mark Word
的拷貝(JVM會將對象頭中的Mark Word
拷貝到鎖記錄中,官方稱為Displaced Mark Ward
)這個時候線程堆棧與對象頭的狀態如圖

如上圖所示:如果當前鎖對象沒有被線程鎖定,那么鎖標志位為01狀態,JVM在執行當前線程時,首先會在當前線程棧幀中創建鎖記錄Lock Record
的空間用於存儲鎖對象目前的Mark Word
的拷貝。
當獲取到鎖的時候,虛擬機使用CAS操作將標記字段Mark Word拷貝到當前線程的鎖記錄中,並且將Mark Word
更新為指向當前線程Lock Record
的指針。如果更新成功了,那么這個線程就有用了該對象的鎖,並且對象Mark Word的鎖標志位更新為(Mark Word
中最后的2bit)00,即表示此對象處於輕量級鎖定狀態,如圖:

如果這個更新操作失敗,JVM會檢查當前的鎖對象的Mark Word
中是否存在指向當前線程的棧幀的指針,如果有,說明該鎖已經被獲取,可以直接調用。如果沒有,則說明該鎖被其他線程搶占了,如果有兩條以上的線程競爭同一個鎖,那輕量級鎖就不再有效,直接膨脹位重量級鎖,沒有獲得鎖的線程會被阻塞。此時,鎖的標志位為10
此時鎖對象Mark Word
中存儲的是指向重量級鎖的指針。
輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word
替換回到對象頭中,如果成功,則表示沒有發生競爭關系。如果失敗,表示當前鎖存在競爭關系。鎖就會膨脹成重量級鎖。兩個線程同時爭奪鎖,導致鎖膨脹的流程圖如下:

偏向鎖:
在大多實際環境下,鎖不僅不存在多線程競爭,而且總是由同一個線程多次獲取,那么在同一個線程反復獲取所釋放鎖中,且其中沒有鎖的競爭,這種情況下,多次的獲取鎖和釋放鎖帶來了很多不必要的性能開銷和上下文切換。
為了解決這一問題,HotSpot的作者在Java SE 1.6 中對Synchronized進行了優化,引入了偏向鎖。當一個線程訪問同步代碼塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID
,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖。只需要簡單地測試一下對象頭的Mark Word
里是否存儲着指向當前線程的偏向鎖。如果成功,表示線程已經獲取到了鎖。

偏向鎖的撤銷
偏向鎖使用了一種等待競爭出現才會釋放鎖的機制。所以當其他線程嘗試獲取偏向鎖時,持有偏向鎖的線程才會釋放鎖。但是偏向鎖的撤銷需要等到全局安全點(就是當前線程沒有正在執行的字節碼)。它會首先暫停擁有偏向鎖的線程,讓你后檢查持有偏向鎖的線程是否活着。如果線程不處於活動狀態,直接將對象頭設置為無鎖狀態。如果線程活着,JVM會遍歷棧幀中的鎖記錄,棧幀中的鎖記錄和對象頭要么偏向於其他線程,要么恢復到無鎖狀態或者標記對象不適合作為偏向鎖。

鎖的優缺點對比
鎖 | 優點 | 缺點 | 使用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要CAS操作,沒有額外的性能消耗,和執行非同步方法相比僅存在納秒級的差距 | 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 適用於只有一個線程訪問同步代碼塊的場景 |
輕量級鎖 | 競爭的線程不會阻塞,提高了響應速度 | 如線程成始終得不到鎖競爭的線程,使用自旋會消耗CPU性能 | 追求響應時間,同步塊執行速度非常快 |
重量級鎖 | 線程競爭不適用自旋,不會消耗CPU | 線程阻塞,響應時間緩慢,在多線程下,頻繁的獲取釋放鎖,會帶來巨大的性能消耗 | 追求吞吐量,同步塊執行速度較長 |
Synchronized和Lock
鎖 | 優點 | 缺點 | 使用場景 |
---|---|---|---|
Synchronized | 1.java的關鍵字,jvm原生支持 2.退出代碼塊和異常時都能釋放鎖 |
1.效率低,試圖獲取鎖時不能設定超時時間。多線程競爭一個鎖時,其余未得到鎖的線程只能不停的嘗試獲得鎖,而不能中斷。高並發的情況下會導致性能下降 2.不夠靈活,只能使用某個對象作為鎖,且加鎖和釋放的時機單一 3.無法知道是否成功獲得鎖 4.鎖的類型不可更改:可重入 不可中斷 非公平 |
|
Lock | 1.接口 |
使用Synchronized有哪些要注意的?
- 鎖對象不能為空,因為鎖的信息都保存在對象頭里
- 作用域不宜過大,影響程序執行的速度,控制范圍過大,編寫代碼也容易出錯
- 避免死鎖
- 在能選擇的情況下,既不要用Lock也不要用synchronized關鍵字,用java.util.concurrent包中的各種各樣的類,如果不用該包下的類,在滿足業務的情況下,可以使用synchronized關鍵,因為代碼量少,避免出錯
synchronized是公平鎖嗎?
synchronized實際上是非公平的,新來的線程有可能立即獲得監視器,而在等待區中等候已久的線程可能再次等待,會導致飢餓。