淺談synchronized的實現原理

Synchronized是Java中的重量級鎖,在我剛學Java多線程編程時,我只知道它的實現和monitor有關,但是synchronized和monitor的關係,以及monitor的本質究竟是什麼,我並沒有嘗試理解,而是選擇簡單的略過。在最近的一段時間,由於實際的需要,我又把這個問題翻出來,Google了很多資料,整個實現的過程總算是弄懂了,爲了以防遺忘,便整理成了這篇博客。 在本篇文章中,我將以class文件爲突破口,試圖解釋Synchronized的實現原理。

從java代碼的反彙編說起

很容易的想到,可以從 程序的行爲 來了解synchronized的實現原理。但是在源代碼層面,似乎看不出synchronized的實現原理。鎖與不鎖的區別,似乎僅僅只是有沒有被synchronized修飾。不如把目光放到更加底層的彙編上,看看能不能找到突破口。 javap 是官方提供的.class文件分解器,它能幫助我們獲取.class文件的彙編代碼。具體用法可參考這裏。 接下來我會使用javap命令對*.class文件進行反彙編。 編寫文件Test.java:

public class Test {
private int i = 0;
public void addI_1(){
synchronized (this){
i++;
}
}
public synchronized void addI_2(){
i++;
}
}
複製代碼
生成class文件,並獲取對Test.class反彙編的結果:

javac Test.java
javap -v Test.class
複製代碼
Classfile /Users/zhangkunwei/Desktop/Test.class
Last modified Jul 13, 2018; size 453 bytes
MD5 checksum ada74ec8231c64230d6ae133fee5dd16
Compiled from "Test.java"
... ...
public void addI_1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: dup
6: getfield #2 // Field i:I
9: iconst_1
10: iadd
11: putfield #2 // Field i:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
... ...
public synchronized void addI_2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field i:I
5: iconst_1
6: iadd
7: putfield #2 // Field i:I
10: return
... ...
複製代碼
通過反彙編結果,我們可以看到:

進入被synchronized修飾的語句塊時會執行 monitorenter ,離開時會執行 monitorexit 。
相較於被synchronized修飾的語句塊,被synchronized修飾的方法中沒有指令 monitorenter和 monitorexit ,且flags中多了ACC_SYNCHRONIZED標誌。 monitorenter 和 monitorexit 指令是做什麼的?同步語句塊和同步方法的實現原理有何不同?遇事不決查文檔,看看官方文檔的解釋。
monitorenter

DescriptionThe objectref must be of type reference .

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread > that executes monitorenter attempts to gain ownership of the monitor associated with objectref , as > > follows:

If the entry count of the monitor associated with objectref is zero, the thread enters the monitor > and sets its entry count to one. The thread is then the owner of the monitor.
If the thread already owns the monitor associated with objectref , it reenters the monitor, incrementing its entry count.
If another thread already owns the monitor associated with objectref , the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
Notes

A monitorenter instruction may be used with one or more monitorexit instructions (§monitorexit) to implement a synchronized statement in the Java programming language (§3.14). The monitorenter and monitorexit instructions are not used in the implementation of synchronized methods, although they can be used to provide equivalent locking semantics. Monitor entry on invocation of a synchronized method, and monitor exit on its return, are handled implicitly by the Java Virtual Machine's method invocation and return instructions, as if monitorenter and monitorexit were used.
簡單翻譯一下: 指令 monitorenter 的操作的必須是一個對象的引用,且其類型爲引用。每一個對象都會有一個 monitor 與之關聯,當且僅當 monitor 被(其他(線程)對象)持有時, monitor 會被鎖上。其執行細節是,當一個線程嘗試持有某個對象的 monitor 時:

如果該對象的 monitor 中的 entry count ==0,則將 entry count 置1,並令該線程爲 monitor 的持有者。
如果該線程已經是該對象的 monitor 的持有者,那麼重新進入 monitor ,並使得 entry count 自增一次。
如果其他線程已經持有該對象的 monitor ,則該線程將會被阻塞,直到 monitor 中的 entry count ==0,然後重新嘗試持有。 注意: monitorenter 必須與一個以上 monitorexit 配合使用來實現Java中的同步語句塊。而同步方法卻不是這樣的:同步方法不使用 monitorenter 和 monitorexit 來實現。當同步方法被調用時, Monitor 介入;當同步方法return時, Monitor 退出。這兩個操作,都是被 JVM 隱式的handle的,就好像這兩個指令被執行了一樣。
monitorexit

Description

The objectref must be of type reference .
The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref .
The thread decrements the entry count of the monitor associated with objectref . If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
簡單翻譯一下: 指令 monitorenter 的操作的必須是一個對象的引用,且其類型爲引用。並且:

執行 monitorexit 的線程必須是 monitor 的持有者。
執行 monitorexit 的線程讓 monitor 的 entry count 自減一次。如果最後 entry count ==0,這個線程就不再是 monitor 的持有者,意味着其他被阻塞線程都能夠嘗試持有 monitor
根據以上信息,上面的疑問得到了解釋:

monitorenter和 monitorexit 是做什麼的? monitorenter 能“鎖住”對象。當一個線程獲取 monitor 的鎖時,其他請求訪問共享內存空間的線程無法取得訪問權而被阻塞; monitorexit能“解鎖”對象,喚醒因沒有取得共享內存空間訪問權而被阻塞的線程。
爲什麼一個 monitorenter 與多個 monitorexit 對應,是一對多,而不是一一對應? 一對多的原因,是爲了保證:執行 monitorenter 指令,後面一定會有一個 monitorexit 指令被執行。上面的例子中,程序正常執行,在離開同步語句塊時執行第一個 monitorexit ;Runtime期間程序拋出Exception或Error,而後執行第二個 monitorexit 以離開同步語句塊。
爲什麼同步語句塊和同步方法的反彙編代碼略有不同? 同步語句塊是使用 monitorenter 和 monitorexit 實現的;而同步方法是 JVM 隱式處理的,效果與 monitorenter 和 monitorexit 一樣。並且,同步方法的flags也不一樣,多了一個ACC_SYNCHRONIZED標誌,這個標誌是告訴 JVM :這個方法是一個同步方法,可以參考這裏。
Monitor

在上一個部分,我們容易得出一個結論:synchronized的實現和 monitor 有關。 monitor 又是什麼呢?從文檔的描述可以看出, monitor 類似於操作系統中的 互斥量 這個概念:不同對象對共享內存空間的訪問是互斥的。在 JVM ( Hotspot )中, monitor 是由 ObjectMonitor 實現,其主要的數據結構如下:

ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; //指向當前monitor的持有者
_WaitSet = NULL; //持有monitor後,調用的wait()的線程集合
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //嘗試持有monitor失敗後被阻塞的線程集合
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
複製代碼
可以看出,我們可以

通過修改_owner來指明 monitor 鎖的擁有者;
通過讀取_EntryList來獲取因獲取鎖失敗而被阻塞的線程集合;
通過讀取_WaitSet來獲取在獲得鎖後主動放棄鎖的線程集合。
到這裏,synchronized的實現原理已經基本理清楚了,但是還有一個未解決的疑問:線程是怎麼知道 monitor 的地址的?線程只有知道它的地址,才能夠訪問它,然後才能與以上的分析聯繫上。答案是 monitor 的地址在Java對象頭中。

Java對象頭

在Java中,每一個對象的組成成分中都有一個Java對象頭。通過對象頭,我們可以獲取對象的相關信息。 這是Java對象頭的數據結構(32位虛擬機下):

其中的Mark Word,它是一個可變的數據結構,即它的數據結構是依情況而定的。下面是在對應的鎖狀態下,Mark Word的數據結構(32位虛擬機下):

synchronized是一個重量級鎖,所以對應圖中的重量級鎖狀態。其中有一個字段是:指向重量級鎖的指針,共佔用25+4+1=30bit,它的內容就是這個對象的引用所關聯的 monitor 的地址。 線程可以通過Java對象頭中的Mark Word字段,來獲取 monitor 的地址,以便獲得鎖。

回到最初的問題

synchronized的實現原理是什麼?從上面的分析來看,答案已經顯而易見了。當多個線程一起訪問共享內存空間時,這些線程可以通過synchronized鎖住 對象 的對象頭中,根據Mark Word字段來訪問該對象所關聯的 monitor ,並嘗試獲取。當一個線程成功獲取 monitor 後,其他與之競爭 monitor 持有權的線程將會被阻塞,並進入EntryList。當該線程操作完畢後,釋放鎖,因爭用 monitor 失敗而被阻塞的線程就會被喚醒,然後重複以上步驟。

寫在最後

我發現其實大部分答案都可以從文檔中得到,所以以後遇到問題還是要嘗試從文檔中找到答案。 本人水平有限,如果本文有錯誤,還望指正,謝謝~

需要java學習路線圖的私信筆者“java”領取哦!另外喜歡這篇文章的可以給筆者點個贊,關注一下,每天都會分享Java相關文章!還有不定時的福利贈送,包括整理的學習資料,面試題,源碼等~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章