Java中synchronized關鍵字你知道多少

  • 2019 年 10 月 3 日
  • 筆記

1.什麼是synchronized

我們將其理解為同步鎖,可以實現共享資源的同步訪問,解決執行緒並發的安全問題。synchronize翻譯成中文:同步,使同步。synchronized:已同步。

1.1 怎麼使用的

  • 修飾實例方法,作用於當前對象實例加鎖,進入同步程式碼前要獲得當前對象實例的鎖
  • 修飾靜態方法,作用於當前類對象加鎖,進入同步程式碼前要獲得當前類對象的鎖 。也就是給當前類加鎖,會作用於類的所有對象實例,因為靜態成員不屬於任何一個實例對象,是類成員( static 表明這是該類的一個靜態資源,不管new了多少個對象,只有一份,所以對該類的所有對象都加了鎖)。所以如果一個執行緒A調用一個實例對象的非靜態 synchronized 方法,而執行緒B需要調用這個實例對象所屬類的靜態 synchronized 方法,是允許的,不會發生互斥現象,因為訪問靜態 synchronized 方法佔用的鎖是當前類的鎖,而訪問非靜態 synchronized 方法佔用的鎖是當前實例對象鎖。
  • 修飾程式碼塊,指定加鎖對象,對給定對象加鎖,進入同步程式碼塊前要獲得給定對象的鎖。 和 synchronized 方法一樣,synchronized(this)程式碼塊也是鎖定當前對象的。synchronized 關鍵字加到 static 靜態方法和 synchronized(class)程式碼塊上都是是給 Class 類上鎖。這裡再提一下:synchronized關鍵字加到非 static 靜態方法上是給對象實例上鎖。另外需要注意的是:盡量不要使用 synchronized(String a) 因為JVM中,字元串常量池具有緩衝功能!

 

2.早期的synchronized

JDK1.6之前屬於重量級鎖,依賴於作業系統的Mutex Lock,Java的執行緒映射到作業系統的原生執行緒,需要作業系統申請互斥量,作業系統對執行緒的切換,需要從用戶態切換到內核態,比較耗時,效率底下。

 

3.對synchronized的優化

JDK1.6之後在JVM層面對synchronized底層做了很多的優化,包括偏向鎖,輕量級鎖,自旋鎖,自適應自旋鎖,鎖消除,鎖粗化等優化技術。

3.1 偏向鎖

目的:在沒有執行緒競爭的情況下,減少傳統的重量級鎖使用作業系統互斥量的開銷,提升性能。

特點:

  1. 在沒有鎖競爭的情況下,會把整個鎖消除
  2. 偏向於第一個獲取到偏向鎖的執行緒
  3. 如果在接下來的執行中偏向鎖沒有被其他執行緒獲取,那麼擁有該鎖的執行緒就不需要同步

變化:在鎖競爭激烈的場合,偏向鎖失效。原因是,在此情況下,極有可能每次申請鎖的執行緒不是同一個執行緒,所以此時不應該使用偏向鎖,否則得不償失。But,偏向鎖失效後,並不會立即膨脹為重量級鎖,而是首先升級為輕量級鎖。關於偏向鎖的原理可以查看《深入理解Java虛擬機:JVM高級特性與最佳實踐》第二版的13章第三節鎖優化。

3.2 輕量級鎖

當偏向鎖失效,JVM不會立即升級為重量級鎖,而是試圖使用輕量級鎖的優化手段(JDK1.6之後加入的)。輕量級鎖不是為了替代重量級鎖,它的本意是是在沒有執行緒競爭的情況下,減少傳統的重量級鎖使用作業系統互斥量的開銷,提升性能。

目的:和偏向鎖一樣

特點:

  1. 和偏向鎖不同,輕量級鎖使用CAS操作代替重量級鎖。
  2. 使用輕量級鎖,不需要申請互斥量。
  3. 輕量級鎖的加鎖和釋放鎖都是CAS操作。

變化:對於大多數鎖來說,在整個同步周期都不存在競爭,這來自經驗數據。如果沒有競爭,輕量級鎖使用CAS操作,避免了使用互斥鎖的開銷。如果存在競爭,除了互斥鎖的開銷,還會有額外的CAS操作,所以如果存在鎖競爭,輕量級鎖比重量級鎖更慢。如果競爭激烈輕量級鎖會迅速膨脹為重量級鎖。關於輕量級鎖的原理可以查看《深入理解Java虛擬機:JVM高級特性與最佳實踐》第二版的13章第三節鎖優化。

3.3 自旋鎖和自適應自旋鎖

輕量級鎖失效後,JVM避免執行緒真的在作業系統層面掛起,還會進行一項成為自旋鎖的優化手段。在JDK1.6之前就有這項技術了,只是他是默認關閉的,可以通過參數--XX:+UseSpinning開啟。JDK1.6之後默認開啟。自旋不能完全替代阻塞,因為它還要佔用處理器的時間。如果鎖被佔用的時間短,那麼自旋鎖的效果就好;否則,反之。自旋等待的時間必須固定,如果超過限定的次數,仍然沒有獲取到鎖,就掛起執行緒。自旋默認10次,可以使用參數--XX:PreBlockSpin修改。

3.3.1 為什麼會有自旋鎖

互斥同步對性能最大的影響是阻塞的實現,因為執行緒的掛起和恢復都需要轉入內核態去完成(用戶態到內核態的轉換將會耗費一定的時間)。而一般執行緒持有鎖的時間並不會太長,如果僅僅為了這一點時間而掛起或恢復執行緒將會得不償失。所以JVM團隊就想:"能否讓後面來的請求獲取鎖的執行緒等待一會兒而不被掛起?看看持有鎖的執行緒是否很快就會釋放鎖"。

目的:為了減少執行緒的掛起和恢復,減少帶來的系統開銷,引入自旋鎖。

3.3.2 如何實現自旋

為了讓一個執行緒等待,我們只需要讓執行緒執行一個忙循環(自旋),這項技術就叫做自旋。

3.3.3 自旋的特點

  1. 執行忙循環
  2. 自旋次數固定(默認10次)
  3. JDK1.6之前默認關閉,之後默認打開
  4. 效果的好壞依賴於鎖被佔用的時間的長短

3.3.4 自適應自旋鎖

另外,在JDK1.6時候引入了自適應自旋鎖。改進:自旋次數不是固定的。根據上次同一個鎖的自旋次數和鎖的擁有者的狀態來確定自旋次數。JVM變得越來越聰明了。與自旋鎖的區別就是自旋次數不固定。

3.4 鎖消除

即使JVM正在運行,如果檢測到共享數據不可能存在競爭,將會執行鎖消除操作。這將會節省毫無意義的請求鎖的時間。

3.5 鎖粗化

原則上我們寫程式碼,總是建議將Synchronized程式碼塊的作用範圍限制的盡量小,只在共享數據的實際作用域才進行同步,使需要同步的操作數盡量小,如果存在競爭,等待執行緒也會儘快拿到鎖。大部分情況下,上面的原則沒有問題,但是如果一些列的連續操作都對同一個對象反覆加鎖解鎖,會帶來很多不必要的性能消耗。

4.Synchronized和ReenTrantLock對比

① 兩者都是可重入鎖

兩者都是可重入鎖。“可重入鎖”概念是:自己可以再次獲取自己的內部鎖。比如一個執行緒獲得了某個對象的鎖,此時這個對象鎖還沒有釋放,當其再次想要獲取這個對象的鎖的時候還是可以獲取的,如果不可鎖重入的話,就會造成死鎖。同一個執行緒每次獲取鎖,鎖的計數器都自增1,所以要等到鎖的計數器下降為0時才能釋放鎖。

② synchronized 依賴於 JVM 而 ReenTrantLock 依賴於 API

synchronized 是依賴於 JVM 實現的,前面我們也講到了 虛擬機團隊在 JDK1.6 為 synchronized 關鍵字進行了很多優化,但是這些優化都是在虛擬機層面實現的,並沒有直接暴露給我們。ReenTrantLock 是 JDK 層面實現的(也就是 API 層面,需要 lock() 和 unlock 方法配合 try/finally 語句塊來完成),所以我們可以通過查看它的源程式碼,來看它是如何實現的。

③ ReenTrantLock 比 synchronized 增加了一些高級功能

相比synchronized,ReenTrantLock增加了一些高級功能。主要來說主要有三點:

a.等待可中斷;b.可實現公平鎖;c.可實現選擇性通知(鎖可以綁定多個條件)

  • ReenTrantLock提供了一種能夠中斷等待鎖的執行緒的機制,通過lock.lockInterruptibly()來實現這個機制。也就是說正在等待的執行緒可以選擇放棄等待,改為處理其他事情。
  • ReenTrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的執行緒先獲得鎖。ReenTrantLock默認情況是非公平的,可以通過 ReenTrantLock類的ReentrantLock(boolean fair)構造方法來制定是否是公平的。
  • synchronized關鍵字與wait()和notify/notifyAll()方法相結合可以實現等待/通知機制,ReentrantLock類當然也可以實現,但是需要藉助於Condition介面與newCondition() 方法。Condition是JDK1.5之後才有的,它具有很好的靈活性,比如可以實現多路通知功能也就是在一個Lock對象中可以創建多個Condition實例(即對象監視器),執行緒對象可以註冊在指定的Condition中,從而可以有選擇性的進行執行緒通知,在調度執行緒上更加靈活。在使用notify/notifyAll()方法進行通知時,被通知的執行緒是由 JVM 選擇的,用ReentrantLock類結合Condition實例可以實現“選擇性通知” ,這個功能非常重要,而且是Condition介面默認提供的。而synchronized關鍵字就相當於整個Lock對象中只有一個Condition實例,所有的執行緒都註冊在它一個身上。如果執行notifyAll()方法的話就會通知所有處於等待狀態的執行緒這樣會造成很大的效率問題,而Condition實例的signalAll()方法 只會喚醒註冊在該Condition實例中的所有等待執行緒。

如果你想使用上述功能,那麼選擇ReenTrantLock是一個不錯的選擇。

④ 性能已不是選擇標準

在JDK1.6之前,synchronized 的性能是比 ReenTrantLock 差很多。具體表示為:synchronized 關鍵字吞吐量隨執行緒數的增加,下降得非常嚴重。而ReenTrantLock 基本保持一個比較穩定的水平。我覺得這也側面反映了, synchronized 關鍵字還有非常大的優化餘地。後續的技術發展也證明了這一點,我們上面也講了在 JDK1.6 之後 JVM 團隊對 synchronized 關鍵字做了很多優化。JDK1.6 之後,synchronized 和 ReenTrantLock 的性能基本是持平了。所以網上那些說因為性能才選擇 ReenTrantLock 的文章都是錯的!JDK1.6之後,性能已經不是選擇synchronized和ReenTrantLock的影響因素了!而且虛擬機在未來的性能改進中會更偏向於原生的synchronized,所以還是提倡在synchronized能滿足你的需求的情況下,優先考慮使用synchronized關鍵字來進行同步!優化後的synchronized和ReenTrantLock一樣,在很多地方都是用到了CAS操作。

 

 

Tags: