Java中synchronized關鍵字你知道多少
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 偏向鎖
目的:在沒有執行緒競爭的情況下,減少傳統的重量級鎖使用作業系統互斥量的開銷,提升效能。特點:
- 在沒有鎖競爭的情況下,會把整個鎖消除
- 偏向於第一個獲取到偏向鎖的執行緒
- 如果在接下來的執行中偏向鎖沒有被其他執行緒獲取,那麼擁有該鎖的執行緒就不需要同步
變化:在鎖競爭激烈的場合,偏向鎖失效。原因是,在此情況下,極有可能每次申請鎖的執行緒不是同一個執行緒,所以此時不應該使用偏向鎖,否則得不償失。But,偏向鎖失效後,並不會立即膨脹為重量級鎖,而是首先升級為輕量級鎖。關於偏向鎖的原理可以檢視《深入理解Java虛擬機器:JVM高階特性與最佳實踐》第二版的13章第三節鎖優化。
3.2 輕量級鎖
當偏向鎖失效,JVM不會立即升級為重量級鎖,而是試圖使用輕量級鎖的優化手段(JDK1.6之後加入的)。輕量級鎖不是為了替代重量級鎖,它的本意是是在沒有執行緒競爭的情況下,減少傳統的重量級鎖使用作業系統互斥量的開銷,提升效能。
目的:和偏向鎖一樣
特點:
- 和偏向鎖不同,輕量級鎖使用CAS操作代替重量級鎖。
- 使用輕量級鎖,不需要申請互斥量。
- 輕量級鎖的加鎖和釋放鎖都是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 自旋的特點
- 執行忙迴圈
- 自旋次數固定(預設10次)
- JDK1.6之前預設關閉,之後預設開啟
- 效果的好壞依賴於鎖被佔用的時間的長短
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操作。