眾所周知 synchronized 關鍵字是解決併發問題常用解決方案,有以下三種使用方式:
- 同步普通方法,鎖的是當前對象。
- 同步靜態方法,鎖的是當前 Class 對象。
- 同步塊,鎖的是 () 中的對象。
實現原理:
JVM 是通過進入、退出對象監視器( Monitor )來實現對方法、同步塊的同步的。
具體實現是在編譯之後在同步方法調用前加入一個 monitor.enter 指令,在退出方法和異常處插入 monitor.exit 的指令。
其本質就是對一個對象監視器( Monitor )進行獲取,而這個獲取過程具有排他性從而達到了同一時刻只能一個線程訪問的目的。
而對於沒有獲取到鎖的線程將會阻塞到方法入口處,直到獲取鎖的線程 monitor.exit 之後才能嘗試繼續獲取鎖。
流程圖如下:
通過一段代碼來演示:
1 public static void main(String[] args) {
2 synchronized (Synchronize.class){
3 System.out.println("Synchronize");
4 }
5 }
使用 javap -c Synchronize 可以查看編譯之後的具體信息。
1 public class com.crossoverjie.synchronize.Synchronize {
2 public com.crossoverjie.synchronize.Synchronize();
3 Code:
4 0: aload_0
5 1: invokespecial #1 // Method java/lang/Object."
6 4: return
7 public static void main(java.lang.String[]);
8 Code:
9 0: ldc #2 // class com/crossoverjie/synchronize/Synchronize
10 2: dup
11 3: astore_1
12 **4: monitorenter**
13 5: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
14 8: ldc #4 // String Synchronize
15 10: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16 13: aload_1
17 **14: monitorexit**
18 15: goto 23
19 18: astore_2
20 19: aload_1
21 20: monitorexit
22 21: aload_2
23 22: athrow
24 23: return
25 Exception table:
26 from to target type
27 5 15 18 any
28 18 21 18 any
29 }
可以看到在同步塊的入口和出口分別有 monitorenter,monitorexit
指令。
鎖優化
synchronized 很多都稱之為重量鎖,JDK1.6 中對 synchronized 進行了各種優化,為了能減少獲取和釋放鎖帶來的消耗引入了偏向鎖和輕量鎖。
輕量鎖
當代碼進入同步塊時,如果同步對象為無鎖狀態時,當前線程會在棧幀中創建一個鎖記錄(Lock Record)區域,同時將鎖對象的對象頭中 Mark Word 拷貝到鎖記錄中,再嘗試使用 CAS 將 Mark Word更新為指向鎖記錄的指針。
如果更新成功,當前線程就獲得了鎖。
如果更新失敗 JVM 會先檢查鎖對象的 Mark Word 是否指向當前線程的鎖記錄。
如果是則說明當前線程擁有鎖對象的鎖,可以直接進入同步塊。
不是則說明有其他線程搶佔了鎖,如果存在多個線程同時競爭一把鎖,輕量鎖就會膨脹為重量鎖。
解鎖
輕量鎖的解鎖過程也是利用 CAS 來實現的,會嘗試鎖記錄替換回鎖對象的 Mark Word 。如果替換成功則說明整個同步操作完成,失敗則說明有其他線程嘗試獲取鎖,這時就會喚醒被掛起的線程(此時已經膨脹為重量鎖)
輕量鎖能提升性能的原因:
認為大多數鎖在整個同步週期都不存在競爭,所以使用 CAS 比使用互斥開銷更少。但如果鎖競爭激烈,輕量鎖就不但有互斥的開銷,還有 CAS 的開銷,甚至比重量鎖更慢。
偏向鎖
為了進一步的降低獲取鎖的代價,JDK1.6 之後還引入了偏向鎖。
偏向鎖的特徵是:鎖不存在多線程競爭,並且應由一個線程多次獲得鎖。
當線程訪問同步塊時,會使用 CAS 將線程 ID 更新到鎖對象的 Mark Word 中,如果更新成功則獲得偏向鎖,並且之後每次進入這個對象鎖相關的同步塊時都不需要再次獲取鎖了。
釋放鎖
當有另外一個線程獲取這個鎖時,持有偏向鎖的線程就會釋放鎖,釋放時會等待全局安全點(這一時刻沒有字節碼運行),接著會暫停擁有偏向鎖的線程,根據鎖對象目前是否被鎖來判定將對象頭中的 Mark Word 設置為無鎖或者是輕量鎖狀態。
偏向鎖可以提高帶有同步卻沒有競爭的程序性能,但如果程序中大多數鎖都存在競爭時,那偏向鎖就起不到太大作用。可以使用 -XX:-userBiasedLocking=false 來關閉偏向鎖,並默認進入輕量鎖。
其他優化
適應性自旋
在使用 CAS 時,如果操作失敗,CAS 會自旋再次嘗試。由於自旋是需要消耗 CPU 資源的,所以如果長期自旋就白白浪費了 CPU。JDK1.6加入了適應性自旋:
如果某個鎖自旋很少成功獲得,那麼下一次就會減少自旋。
總結
synchronized 現在已經不像以前那麼重了,拿 1.8 中的 ConcurrentHashMap 就可以看出,裡面大量的使用了 synchronized 來進行同步。
閱讀更多 JavaSpring高級進階 的文章