文章已同步發表于微信公眾號JasonGaoH,synchronized關鍵字的原理
synchronized關鍵字
什么是synchronized
JDK官網對synchronized關鍵字有個比較權威的解釋。
Synchronized keyword enable a simple strategy for preventing thread interference and memory consistency errors: if an object is visible to more than one thread, all reads or writes to that object's variables ard done through synchronized methods.
上述解釋的意思是:synchronized關鍵字可以實現一個簡單的策略來防止線程干擾和內存一致性錯誤,如果一個對象對多個線程是可見的,那么對該對象的所有讀或者寫都將通過同步的方式來進行,具體表現如下:
- synchronized關鍵字提供了一種鎖的機制,能夠確保共享變量的互斥訪問,從而防止數據不一致的問題出現。
- synchronized關鍵字包括monitor enter和monitor exit兩個JVM指令,它能夠保證在任何時候任何線程執行到monitor enter成功之前都必須從主內存中獲取數據,而不是緩存中,在monitor exit運行成功之后,共享變量被更新后的值必須刷入主內存。
- synchronized的執行嚴格遵守java happens-before 規則,一個monitor exit指令之前必定要有一個monitor enter。
synchronized關鍵字的用法
synchronized可以用于對代碼塊或方法進行修飾,而不能夠用于對class以及變量進行修飾。
- 同步方法
public synchronized void sync() {
//...
}
- 同步方法塊
private final Object lock = new Object();
public void sync() {
synchronized(lock) {
//...
}
}
關于同步代碼塊和同步方法的區別之前寫過一個關于這個對比,具體可以看這篇文章。
java中的synchronized(同步代碼塊和同步方法的區別)
深入分析Synchronized關鍵字
線程堆棧分析
synchronized關鍵字提供了一種互斥機制,也就是說在同一時刻,只能有一個線程訪問同步資源。
看下面這段程序:
import java.util.concurrent.TimeUnit;
public class TestSync {
private final static Object lock = new Object();
public void accessResource() {
synchronized(lock) {
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final TestSync sync = new TestSync();
for(int i =0;i<5;i++) {
new Thread(){
@Override
public void run() {
sync.accessResource();
}
}.start();
}
}
}
上面的代碼定義一個方法accessResource,并且使用synchronized來對代碼進行同步,同時定義了5個線程調用accessResource方法,由于synchronized的互斥性,只能有一個線程獲得lock的monitor鎖,其他線程只能進入阻塞狀態,等待獲取lock的monitor鎖。
針對這個monitor鎖我們如何從線程堆棧信息來看呢?
其實,jstack命令在Java中可以用來打印進程的線程堆棧信息。
我們來運行這個Java程序,在終端通過top命令查看運行起來的Java程序的進程id,然后執行jstack ‘pid’。
我們來看下打印出來的信息:
通過截圖可以看到Thread-0持有monitor<0x00000007955f2130>的鎖并且處于休眠狀態中,而其他幾個線程則是處于BLOCKED狀態中,它們是在等待著獲取monitor<0x00000007955f2130>的鎖。
JVM指令分析
從JVM指令角度再來分析synchronized關鍵字。
我們可以使用javap這個命令來對上面這個TestSync類生成的class字節碼進行反編譯,得到下面的JVM指令。
Compiled from "TestSync.java"
public class main.TestSync {
static {};
Code:
0: new #3 // class java/lang/Object
3: dup
4: invokespecial #10 // Method java/lang/Object."<init>":()V
7: putstatic #13 // Field lock:Ljava/lang/Object;
10: return
public main.TestSync();
Code:
0: aload_0
1: invokespecial #10 // Method java/lang/Object."<init>":()V
4: return
public void accessResource();
Code:
0: getstatic #13 // Field lock:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter
6: getstatic #20 // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUnit;
9: ldc2_w #26 // long 10l
12: invokevirtual #28 // Method java/util/concurrent/TimeUnit.sleep:(J)V
15: goto 23
18: astore_2
19: aload_2
20: invokevirtual #32 // Method java/lang/InterruptedException.printStackTrace:()V
23: aload_1
24: monitorexit
25: goto 31
28: aload_1
29: monitorexit
30: athrow
31: return
Exception table:
from to target type
6 15 18 Class java/lang/InterruptedException
6 25 28 any
28 30 28 any
public static void main(java.lang.String[]);
Code:
0: new #1 // class main/TestSync
3: dup
4: invokespecial #44 // Method "<init>":()V
7: astore_1
8: iconst_0
9: istore_2
10: goto 27
13: new #45 // class main/TestSync$1
16: dup
17: aload_1
18: invokespecial #47 // Method main/TestSync$1."<init>":(Lmain/TestSync;)V
21: invokevirtual #50 // Method main/TestSync$1.start:()V
24: iinc 2, 1
27: iload_2
28: iconst_5
29: if_icmplt 13
32: return
}
從上面的指令中可以看到,在accessResource()方法中,先后出現了一個monitor enter和兩個monitor exit。
我們主要選取accessResource()這部分代碼塊來重點分析。
public void accessResource();
Code:
0: getstatic #13 //①獲取lock
3: dup
4: astore_1
5: monitorenter //②執行monitorenter JVM指令
6: getstatic #20 // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUnit;
9: ldc2_w #26 // long 10l
12: invokevirtual #28 // Method java/util/concurrent/TimeUnit.sleep:(J)V
15: goto 23 //③跳轉到23行
18: astore_2
19: aload_2
20: invokevirtual #32 // Method java/lang/InterruptedException.printStackTrace:()V
23: aload_1 //④
24: monitorexit //⑤ 執行monitor exit JVM指令
25: goto 31
28: aload_1
29: monitorexit
30: athrow
31: return
首先①獲取到lock引用,然后執行②monitorenter JVM指令,休眠結束后goto至③monitorexit的位置
(astore_n表示存儲引用到本地變量表;aload_n表示從本地變量表加載應用;getstatic表示從class中獲取靜態屬性)
monitorenter
每一個對象都與一個monitor相關聯,一個monitor的lock的鎖只能被一個線程在同一時間獲得,在一個線程嘗試獲得與對象關聯的monitor的所有權時會發生如下的幾件事情。
- 如果monitor的計數器為0,則意味著該monitor的lock還沒有被獲得,,某個線程獲得之后將立即對該計數器加一,從此該線程就是這個monitor的所有者了。
- 如果一個已經擁有該線程所有權的線程重入,則會導致monitor的計數器再次累加。
- 如果monitor已經被其他線程所擁有,則其他線程嘗試獲取該monitor所有權時,會被陷入阻塞狀態直到monitor變為0,才能再次嘗試獲取對monitor的所有權。
monitorexit
釋放對monitor的所有權,想要釋放某個對象關聯的monitor所有權的前提是,你曾經擁有了所有權。釋放monitor所有權的過程比較簡單,就是將monitor的計數器減一,如果計數器的結果為0,則意味著該線程不在擁有對該monitor的所有權,通俗地講就是解鎖。
synchronized的鎖優化
在虛擬機規范對monitorenter和monitorexit的行為描述中,有兩點是需要特別注意的,首先,synchronized同步塊對于同一條線程是可重入的,不會出現自己鎖死自己的問題。其次,同步塊在已進入的線程執行完以前,會阻塞后面其他線程的進入。
Java的線程是映射到操作系統線程上的,要阻塞或喚醒一個線程,都需要操作系統來幫忙完成,這就需要從用戶態切到核心態,因此狀態轉換需要耗費很多的處理器時間,對于簡單的同步塊(如被synchronized修飾的getter或setter方法),狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長。所以synchronized是Java語言中的一個重量級的操作。
其實大多數時候,共享數據的鎖定狀態一般只會持續很短的一段時間,為了這段時間去掛起和恢復線程其實并不值得。
如果物理機上有多個處理器,可以讓多個線程同時執行的話。我們就可以讓后面來的線程“稍微等一下”,但是并不放棄處理器的執行時間,看看持有鎖的線程會不會很快釋放鎖。這個“稍微等一下”的過程就是自旋。
自旋鎖在JDK 1.4中已經引入,在JDK 1.6中默認開啟。只是將當前線程不停地執行循環體,不進行線程狀態的改變,所以響應速度更快,因為上面剛說到,線程的狀態切換會耗費很多CPU時間。但當線程數不停增加時,性能下降明顯,因為每個線程都需要執行,占用CPU時間。如果線程競爭不激烈,并且保持鎖的時間段,適合使用自旋鎖。