前言
在前面文章 synchronized關鍵字的四種加鎖方式中介紹了四種synchronized的使用和區別,但是效果都是一樣的,今天我們更加深入的看一看synchronized背後的’'monitor"鎖,以及和Lock的區別。
synchronized背後的’'monitor"鎖
synchronized的使用非常簡單,僅需要在方法或者代碼塊中使用synchronized就可以了,使用看起來似乎很簡單的背後是Java團隊背後爲我們做了很多的努力來簡化我們的使用。synchronized如何使用直接影響了程序的效率。
因爲每一個Java對象內部都只有一個鎖,如果使用synchronized使一個線程獲取了某一個對象的鎖之後,其它線程就無法獲取這個對象的鎖了,因爲只有一個鎖,其他線程只能等這個線程釋放這個鎖。
首先我們看synchronized在同步代碼塊中的使用背後的字節碼,源碼如下:
public class TestSynchronized {
public void insert(Thread thread) {
synchronized (this) {
}
}
public void insert1(Thread thread) {
synchronized (TestSynchronized.class) {
}
}
}
- 這裏有兩個使用同步代碼塊加鎖的方法,我們使用cmd命令切換到類
TestSynchronized
對應的目錄下,使用編譯命令javac TestSynchronized.java
,這時候會出現一個編譯後的字節碼文件TestSynchronized.class
,使用命令javap -verbose TestSynchronized.class
就可以查看字節碼文件 如下:
Classfile /D:/develop/androidstudio/Forward/app/src/main/java/com/oman/forward/study/TestSynchronized.class
Last modified 2020-3-29; size 614 bytes
MD5 checksum a93f484683fe2ce446f688c977d73f1a
Compiled from "TestSynchronized.java"
public class com.oman.forward.study.TestSynchronized
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#19 // java/lang/Object."<init>":()V
#2 = Class #20 // com/oman/forward/study/TestSynchronized
#3 = Class #21 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 synMethod
#9 = Utf8 (Ljava/lang/Thread;)V
#10 = Utf8 StackMapTable
#11 = Class #20 // com/oman/forward/study/TestSynchronized
#12 = Class #22 // java/lang/Thread
#13 = Class #21 // java/lang/Object
#14 = Class #23 // java/lang/Throwable
#15 = Utf8 synMethod1
#16 = Utf8 synMethod2
#17 = Utf8 SourceFile
#18 = Utf8 TestSynchronized.java
#19 = NameAndType #4:#5 // "<init>":()V
#20 = Utf8 com/oman/forward/study/TestSynchronized
#21 = Utf8 java/lang/Object
#22 = Utf8 java/lang/Thread
#23 = Utf8 java/lang/Throwable
{
public com.oman.forward.study.TestSynchronized();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
public void synMethod(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: aload_0
1: dup
2: astore_2
3: monitorenter
4: aload_2
5: monitorexit
6: goto 14
9: astore_3
10: aload_2
11: monitorexit
12: aload_3
13: athrow
14: return
Exception table:
from to target type
4 6 9 any
9 12 9 any
LineNumberTable:
line 11: 0
line 13: 4
line 14: 14
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 9
locals = [ class com/oman/forward/study/TestSynchronized, class java/lang/Thread, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public void synMethod1(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: ldc #2 // class com/oman/forward/study/TestSynchronized
2: dup
3: astore_2
4: monitorenter
5: aload_2
6: monitorexit
7: goto 15
10: astore_3
11: aload_2
12: monitorexit
13: aload_3
14: athrow
15: return
Exception table:
from to target type
5 7 10 any
10 13 10 any
LineNumberTable:
line 17: 0
line 19: 5
line 20: 15
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 10
locals = [ class com/oman/forward/study/TestSynchronized, class java/lang/Thread, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public void synMethod2(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 24: 0
}
SourceFile: "TestSynchronized.java"
- 我們重點關注synMethod和synMethod1,他們使用的是同步代碼塊加鎖的方式,我們看到他們裏面比synMethod2普通的方法多了兩個指令,分別是monitorenter 和 monitorexit 指令,而且有兩個monitorexit指令,之所以有兩個monitorexit指令的原因是monitorenter只需要插入到同步代碼塊開始的位置,而monitoreexit需要在同步代碼塊正常結束和異常的位置都插入,這樣就可以在異常的時候也釋放鎖。
- 每個對象中都有一個維護着被鎖次數的計數器,monitorenter代表計數器加1,monitorexit代表計數器減1,如果計數器的值爲0的話,就代表着這個對象未被線程使用。
- monitorenter 代表開始加鎖,意味着執行monitorenter的線程開始嘗試獲取對象的monitor的所有權,加鎖分爲幾種情況:
- 如果對象中monitor的計數器爲0,代表此對象沒有被使用,則執行monitorenter的線程就可以佔有這個對象的monitor。
- 如果對象中monitor的計數器不爲0,並且此線程已經持有此對象的monitor的話,那麼monitor計數器就累計加1。
- 如果對象中的monitor計數器不爲0,並且其他線程已經持有了此對象的monitor的話,那麼此線程就處於BLOCKED狀態,需要等待這個monitor計數器的值爲0,值爲0意味着這個monitor已經被釋放了,那麼其它等待這個monitor的線程就可以再次嘗試獲取monitor的所有權了。
- monitorexit 意味着將對象中的monitor計數器減1,上面分析了,當計數器的值爲0的時候意味着這個monitor已經被釋放了,那麼其它等待這個monitor的線程就可以再次嘗試獲取monitor的所有權了。
接下來我們看synchronized在同步方法中使用背後的字節碼,源碼如下:
public class TestSynchronized {
public void synMethod2(Thread thread) {
}
public synchronized void synMethod3(Thread thread) {
}
public static synchronized void synMethod4(Thread thread) {
}
}
- 這裏有兩個使用同步方法加鎖的方法,我們還是和上面的命令一樣,使用cmd命令切換到類
TestSynchronized
對應的目錄下,使用編譯命令javac TestSynchronized.java
,這時候會出現一個編譯後的字節碼文件TestSynchronized.class
,使用命令javap -verbose TestSynchronized.class
就可以查看字節碼文件 如下:
Classfile /D:/develop/androidstudio/Forward/app/src/main/java/com/oman/forward/study/TestSynchronized.class
Last modified 2020-3-29; size 409 bytes
MD5 checksum cf108dae480c6b6d58d6cfc8bb7a9c01
Compiled from "TestSynchronized.java"
public class com.oman.forward.study.TestSynchronized
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#14 // java/lang/Object."<init>":()V
#2 = Class #15 // com/oman/forward/study/TestSynchronized
#3 = Class #16 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 synMethod2
#9 = Utf8 (Ljava/lang/Thread;)V
#10 = Utf8 synMethod3
#11 = Utf8 synMethod4
#12 = Utf8 SourceFile
#13 = Utf8 TestSynchronized.java
#14 = NameAndType #4:#5 // "<init>":()V
#15 = Utf8 com/oman/forward/study/TestSynchronized
#16 = Utf8 java/lang/Object
{
public com.oman.forward.study.TestSynchronized();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 8: 0
public void synMethod2(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 24: 0
public synchronized void synMethod3(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 29: 0
public static synchronized void synMethod4(java.lang.Thread);
descriptor: (Ljava/lang/Thread;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 33: 0
}
SourceFile: "TestSynchronized.java"
- 我們重點關注synMethod3和synMethod4,我們看到他們兩個和synMethod2的主要區別在於多了一個flag爲
ACC_SYNCHRONIZED
的標記,這個標記用來標識它是同步方法的。而synMethod3和synMethod4的區別則是synMethod4比synMethod3多了一個ACC_STATIC
的靜態標識符。
小結
- 使用synchronized修飾的方法會有一個
ACC_SYNCHRONIZED
標識,當此方法在準備執行的時候,會發現此方法包含ACC_SYNCHRONIZED
標記,那麼就需要先獲取monitor鎖(如果是非靜態的話,是對象monitor,如果修飾的是靜態方法則是類monitor),獲取monitor之後才能執行方法體中的內容,方法執行結束後會釋放monitor。 - 使用synchronized修飾同步代碼塊的話,會有兩個標識,monitorenter和monitorexit。monitorenter 代表開始加鎖,意味着執行monitorenter的線程開始嘗試獲取對象的monitor的所有權,獲取到monitor遇到的幾種情況上面已經詳細分析了,當同步代碼塊執行結束後,就會進入monitorexit釋放鎖。
- 上面分析得知synchronized如果對於方法加鎖的話,因爲同步的範圍過於大,將會影響程序的性能,所以在編程中可以考慮使用同步代碼塊來替代同步方法,這樣對程序的性能會有提升。
synchronized和Lock的區別
synchronized和Lock都是用來保證線程安全的,下面我們比較它們主要的相同點和不同點。
- 相同點:
- synchronized和Lock都是用來保證線程安全的: Java併發編程的三要素包括原子性,可見性,有序性。因爲使用synchronized和Lock在同一個時間,只能有一個線程執行代碼,所以就保證了原子性,可見性和有序性,也就保證了線程安全。
- synchronized和Lock都是可重入鎖:可重入鎖指的是如果線程獲得了某一個對象的monitor,當再次試圖獲取這個對象的monitor的時候,是不需要先釋放之前的monitor的,上面我們分析synchronized說到monitorenter的時候,說到了monitor計數器可以累加,說明synchronized是可重入鎖,Lock也是可重入鎖。
- 不同點:
- 靈活性不同: lock可以使用tryLock(time)等方法,如果獲取不到鎖的時候,可以去做其它的事情。而synchronized只能選擇等待或者異常。
- 加鎖的顯示和隱式: Lock必須顯示的執行加鎖和解鎖,爲了防止發生死鎖,解鎖一般在finally中執行。而synchronized的加鎖和解鎖是Java虛擬機內部實現的,通過上面的class字節碼分析,本質上也是加鎖和解鎖的操作,並且monitorexit有兩次,所以能夠保證異常釋放鎖,只不過這些內容在代碼上是不像Lock直接體現的。
- 加鎖解鎖的順序: synchronized的加鎖和解鎖,是按順序出現的,而Lock可以加多個所鎖,但是解鎖不用按照順序,可以把先加的鎖後解。比如下面的代碼:
//synchronized 有順序要求 synchronized(obj1) { synchronized(obj1) { //do something } } lock無順序要求 lock1.lock(); lock2.lock(); // do something lock2.unlock(); lock1.unlock();
- 被佔有的線程個數:因爲每個對象只有一個monitor,所以synchronized只能被一個線程佔有。而lock沒有這個限制。比如ReentrantReadWriteLock可以同時被多個線程持有讀鎖。
- 公平性: Lock可以設置公平鎖和非公平鎖,比如ReentrantLock默認是不公平鎖,可以通過構造方法設置公平鎖,而synchronized不能設置。
小結
對synchronized和Lock說了這麼多,如何選擇呢,在我看來有以下幾條建議:
- 從效率考慮: Java在包java.util.concurrent下爲我們提供了很多的併發類,如果使用的話首先考慮使用這些類(前提是足夠了解這些類),比如考慮ConcurrentHashMap等替換HashMap。
- 安全性上考慮: 因爲synchronized使用簡單,而且虛擬機內置幫我們處理了解鎖的操作,就算是異常情況下也會解鎖,所以從編碼上來看的話,synchronized比Lock更加安全(因爲Lock可能會忘記在finally中解鎖)。
- 從靈活性上考慮: Lock有更多的API可供更復雜的需求選擇,如果你需要比如超時功能,可以考慮使用Lock。