1.同步的語義
下面的內(nèi)容摘自JSR 133 FAQ:
Synchronization has several aspects. The most well-understood is mutual exclusion -- only one thread can hold a monitor at once, so synchronizing on a monitor means that once one thread enters a synchronized block protected by a monitor, no other thread can enter a block protected by that monitor until the first thread exits the synchronized block.
同步有幾個方面骚腥。最容易理解的是互斥 —— 只有一個線程可以立即持有一個監(jiān)視器,因此在監(jiān)視器上進行同步意味著一旦一個線程進入由一個監(jiān)視器保護的同步塊贫导,則其他線程都不能進入該監(jiān)視器保護的塊练对,直到第一個線程退出同步塊拴清。
But there is more to synchronization than mutual exclusion. Synchronization ensures that memory writes by a thread before or during a synchronized block are made visible in a predictable manner to other threads which synchronize on the same monitor. After we exit a synchronized block, we release the monitor, which has the effect of flushing the cache to main memory, so that writes made by this thread can be visible to other threads. Before we can enter a synchronized block, we acquire the monitor, which has the effect of invalidating the local processor cache so that variables will be reloaded from main memory. We will then be able to see all of the writes made visible by the previous release.
但是同步不僅僅是互斥。 同步確保以可預(yù)見的方式,使線程在同步塊之前或期間對內(nèi)存的寫入對于在同一監(jiān)視器上同步的其他線程可見。 退出同步塊后壳嚎,我們 釋放 該監(jiān)視器,其有將緩存刷新到主內(nèi)存的效果末早, 以便該線程進行的寫入對于其他線程可見烟馅。 在我們進入一個同步塊之前,我們需要 獲取 該監(jiān)視器然磷,該監(jiān)視器具有使本地處理器緩存無效的作用焙糟,以便可以從主內(nèi)存中重新加載變量。 然后样屠,我們將能夠看到以前釋放中所有可見的寫入。
Discussing this in terms of caches, it may sound as if these issues only affect multiprocessor machines. However, the reordering effects can be easily seen on a single processor. It is not possible, for example, for the compiler to move your code before an acquire or after a release. When we say that acquires and releases act on caches, we are using shorthand for a number of possible effects.
從高速緩存的角度進行討論缺脉,聽起來似乎這些問題僅影響多處理器計算機痪欲。 但是,重排序效果可以在單個處理器上輕松看到攻礼。 例如业踢,編譯器不可能在獲取之前或釋放之后移動代碼。 當(dāng)我們說獲取和釋放作用于緩存時礁扮,我們使用簡寫來表示多種可能的影響知举。
The new memory model semantics create a partial ordering on memory operations (read field, write field, lock, unlock) and other thread operations (start and join), where some actions are said to happen before other operations. When one action happens before another, the first is guaranteed to be ordered before and visible to the second. The rules of this ordering are as follows:
新的內(nèi)存模型語義在內(nèi)存操作(讀字段,寫字段太伊,鎖定雇锡,解鎖)和其他線程操作( start 和 join )上創(chuàng)建了部分排序,其中某些操作據(jù)說 happen before其他操作僚焦。 當(dāng)一個動作在另一個動作之前發(fā)生時锰提,第一個動作被確保排序在第二個動作之前并且對于第二個動作可見。 此排序規(guī)則如下:
Each action in a thread happens before every action in that thread that comes later in the program's order.
線程中的每個動作先于該線程中的在程序順序上后出現(xiàn)的每個動作發(fā)生芳悲。An unlock on a monitor happens before every subsequent lock on that same monitor.
監(jiān)視器上的一個解鎖發(fā)生在 同一個 監(jiān)視器上的每個后續(xù)鎖定之前立肘。A write to a volatile field happens before every subsequent read of that same volatile.
對 volatile 字段的每個寫操作發(fā)生在每次后續(xù)讀取 同一個 volatile之前。A call to start() on a thread happens before any actions in the started thread.
一個對線程的 start() 的調(diào)用發(fā)生在被啟動線程中的任何操作之前名扛。All actions in a thread happen before any other thread successfully returns from a join() on that thread.
線程中的所有操作發(fā)生在其他線程成功從該線程上的 join() 返回之前谅年。
This means that any memory operations which were visible to a thread before exiting a synchronized block are visible to any thread after it enters a synchronized block protected by the same monitor, since all the memory operations happen before the release, and the release happens before the acquire.
這意味著線程在退出同步塊之前對一個線程可見的任何內(nèi)存操作,在進入受同一監(jiān)視器保護的同步塊之后對于任何線程都是可見的肮韧,因為所有內(nèi)存操作都發(fā)生在釋放之前融蹂,而釋放發(fā)生在獲取之前旺订。
可以看到同步的語義包含兩點:一個是互斥,一個是保證可見性殿较。
2 synchronized的基本使用
根據(jù)Java 語言規(guī)范可知:
Java里面的每個對象都關(guān)聯(lián)著一個 monitor耸峭,一個線程可以 lock 或者 unlock這個 monitor。
對于一個類方法淋纲,該方法所在類的Class對象關(guān)聯(lián)的monitor被使用劳闹。
對于一個實例方法,與this(某個調(diào)用該方法的實例對象)關(guān)聯(lián)的monitor被使用洽瞬。
對于一個同步塊本涕,即synchronized(obj){....},與obj關(guān)聯(lián)的monitor被使用。
舉個栗子:
package synchronizedTest;
class Test {
int count;
//實例同步方法
synchronized void bump() {
count++;
}
static int classCount;
//類同步方法
static synchronized void classBump() {
classCount++;
}
}
上面的代碼等價于:
package synchronizedTest;
class BumpTest {
int count;
void bump() {
//同步塊
synchronized (this) { count++; }
}
static int classCount;
static void classBump() {
try {
//同步塊
synchronized (Class.forName("BumpTest")) {
classCount++;
}
} catch (ClassNotFoundException e) {}
}
}
3.從JVM字節(jié)碼層面看同步塊
反解析下上面兩個類對應(yīng)的字節(jié)碼文件伙窃。
編譯成class文件
javac synchronizedTest/Test.java
將Class文件反匯編下
javap -p -v synchronizedTest/Test > synchronizedTest/Test.disasm
類似地:
javac synchronizedTest/BumpTest.java
javap -p -v synchronizedTest/BumpTest > synchronizedTest/BumpTest.disasm
下面重點看下Test.disasm和BumpTest.disasm
BumpTest.bump方法節(jié)選
void bump();
descriptor: ()V
flags:
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 count:I
9: iconst_1
10: iadd
11: putfield #2 // Field count:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table:
from to target type
4 16 19 any
19 22 19 any
LineNumberTable:
line 6: 0
line 7: 24
3.1 monitor_enter的說明:lock特定對象的monitor菩颖。
The 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.
objectref必須是引用類型。
每個對象都與一個監(jiān)視器關(guān)聯(lián)为障。 監(jiān)視器只有在擁有所有者的情況下才被鎖定晦闰。 執(zhí)行monitorenter的線程嘗試獲得與objectref關(guān)聯(lián)的監(jiān)視器的所有權(quán),如下所示:
如果與objectref關(guān)聯(lián)的監(jiān)視器的條目計數(shù)為零鳍怨,則線程進入監(jiān)視器并將其條目計數(shù)設(shè)置為1呻右。 然后,該線程是監(jiān)視器的所有者鞋喇。
如果線程已經(jīng)擁有與objectref關(guān)聯(lián)的監(jiān)視器声滥,則它將重新進入監(jiān)視器,從而條目計數(shù)加1侦香。
如果另一個線程已經(jīng)擁有與objectref關(guān)聯(lián)的監(jiān)視器落塑,則該線程將阻塞,直到該監(jiān)視器的條目計數(shù)為零為止罐韩,然后再次嘗試獲取所有權(quán)憾赁。
3.2 monitor_exit的說明: unlock特定對象的monitor。
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.
objectref必須是引用類型散吵。
執(zhí)行monitorexit的線程必須是與objectref引用的實例相關(guān)聯(lián)的監(jiān)視器的所有者缠沈。
該線程減少與objectref關(guān)聯(lián)的監(jiān)視器的條目計數(shù)。 結(jié)果错蝴,如果條目計數(shù)的值為零洲愤,則線程退出監(jiān)視器,并且不再是其所有者顷锰。 其他被阻塞進入監(jiān)視器的線程可以嘗試進入監(jiān)視器柬赐。
3.3 下面是關(guān)于異常表( Exception table)的說明:
上面是bump方法對應(yīng)的指令,異常表有兩行(如下所示)官紫,每一行稱為異常表條目:
4 16 19 any
19 22 19 any
每個異常表條目監(jiān)控[from, to)的字節(jié)碼肛宋,如果出現(xiàn)異常州藕,則跳轉(zhuǎn)到target指針對應(yīng)的字節(jié)碼執(zhí)行,type則代表該處理器所能捕獲的異常類型(any代表任何異常)酝陈。
對應(yīng)的上面兩個異常條目的意思就是:
- 4對應(yīng)from床玻,16對應(yīng)to, 19對應(yīng)target, any對應(yīng)type沉帮;也就是[4,16)指向的字節(jié)碼指令拋任何異常(any)了锈死,都會跳轉(zhuǎn)到19執(zhí)行。
- 19對應(yīng)from穆壕,22對應(yīng)to, 19對應(yīng)target待牵, any對應(yīng)type;也就是[19, 22)指向的字節(jié)碼拋出任何異常(any)了喇勋,都會跳轉(zhuǎn)到19執(zhí)行缨该。
也就是
- 情況一:[4,16)執(zhí)行沒有任何異常,則goto到24川背,返回贰拿。在這種情況下正常加鎖
3: monitorenter
,釋放鎖15: monitorexit
- 情況二:[4,16)拋出任何異常熄云,都跳轉(zhuǎn)19壮不,都會執(zhí)行到
21: monitorexit
;如果成功了皱碘,則異常結(jié)束;如果在[19, 22)執(zhí)行中拋出任何異常隐孽,就跳轉(zhuǎn)到19再重新執(zhí)行一遍癌椿。
通過上面的分析,我們可以發(fā)現(xiàn)菱阵,不管是否拋出異常踢俄,synchronized 同步塊,都會釋放之前獲取的鎖晴及,也就是 monitorenter 與 monitorexit 始終是成對出現(xiàn)的都办。
BumpTest.classBump和BumpTest.bump是類似,你可以自己嘗試分析下虑稼。
4.從JVM字節(jié)碼層面看同步方法
Test.bump節(jié)選
synchronized void bump();
descriptor: ()V
flags: ACC_SYNCHRONIZED
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: dup
2: getfield #2 // Field count:I
5: iconst_1
6: iadd
7: putfield #2 // Field count:I
10: return
LineNumberTable:
line 6: 0
line 7: 10
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.
Java虛擬機的方法調(diào)用和返回指令琳钉,隱式處理了調(diào)用同步方法時的monitor entry 和返回時的monitor exit,就像使用了monitorenter 和monitorexit 一樣蛛倦。
所以歌懒,同步方法和同步塊的實現(xiàn)方式本質(zhì)上并沒有什么不同。
5.從機器碼看synchronized
假如我們把BumpTest改成如下代碼:
package synchronizedTest;
class BumpTest {
int count;
void bump() {
synchronized (this) { count++; }
}
static int classCount;
static void classBump() {
try {
synchronized (Class.forName("BumpTest")) {
classCount++;
}
} catch (ClassNotFoundException e) {}
}
public static void main(String[] args){
for(int i=0; i< 100000; i++){
classBump();
}
System.out.println(classCount);
}
}
然后重新編譯成字節(jié)碼文件溯壶,再得到本地機器指令文件(注意及皂,執(zhí)行第二條指令還需要hsdis,你可以參考我之前的文章安裝)
javac synchronizedTest/BumpTest.java
java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly synchronizedTest/BumpTest > synchronizedTest/BumpTest.native
然后我們搜索'classBump'
甫男,發(fā)現(xiàn)了下面的機器指令,可以看到在Intel64 CPU 下验烧,synchronized 最終還是用 lock cmpxchg 實現(xiàn)的板驳。
從這里我們可以發(fā)現(xiàn),這里的 synchronized 和之前講的volatile碍拆, Unsafe中的CAS若治,剛好是用類似的原子指令(比如這里的lock cmpxchg)實現(xiàn)的。
至于BumpTest 和Test中其他同步方法或同步塊倔监,你可以試一下直砂,結(jié)果是一致的。
6. 同步
但是浩习,并不是所有的同步都是用上述的原子指令實現(xiàn)的(其實是輕量級鎖)静暂,而是根據(jù)不同情況使用不同的鎖,鎖的類型分為重量級鎖谱秽,輕量級鎖洽蛀,偏向鎖。 下面主要簡單地說明這三種鎖的實現(xiàn)疟赊。
HotSpot JVM 使用一個 two-word 對象頭郊供,第一個word是 Class pointer,第二個是 Mark word 用來保存同步近哟,GC 和 hashCode 等相關(guān)信息驮审。Mark word使用方式見下圖:
6.1 重量級鎖 heavyweight monitor
重量級鎖對應(yīng)上述 Mark word 的 tag bits 為10的情況,即此時狀態(tài)為inflated吉执。
重量級鎖會使用操作系統(tǒng)級別的鎖定原語 ( OS-level locking primitives疯淫, 比如 pthread mutex) 來實現(xiàn)。 這些操作將涉及系統(tǒng)調(diào)用戳玫,需要從操作系統(tǒng)的用戶態(tài)切換至內(nèi)核態(tài)熙掺,其開銷非常之大。重量級鎖可以在所有場景使用咕宿。
6.2 輕量級鎖 lightweight lock
輕量級鎖對應(yīng)上述 Mark word 的 tag bits 為 00 的情況币绩,即此時狀態(tài)為 lightweight-locked。
輕量級鎖是對重量級鎖的優(yōu)化府阀。輕量級鎖使用一個或兩個 CPU 級別的原子指令(比如 lock cmpxchg)充蓝,從而避免了使用操作系統(tǒng)級別的鎖定原語蔫骂。
可選的輕量級鎖實現(xiàn)算法有 Metalock (CAS in both acquire and release), Thin Locks(CAS in acquire), 和 Relaxed-locks (CAS in acquire).
但輕量級鎖適用范圍有限芭碍,以 Thin Locks 為例子垮兑,它適用于這樣的對象,這些對象不被爭用川队,不需要對自己執(zhí)行 wait力细,notify 或 notifyAll 操作睬澡,并且沒有鎖定到過多的嵌套深度。絕大多數(shù)對象都滿足上述條件眠蚂; 那些不滿足條件的對象的鎖要用重量級鎖來實現(xiàn)煞聪。
6.3 偏向鎖 biased lock
偏向鎖在 JDK6 引入,對應(yīng)上述 Mark word 的 tag bits 為 01 的情況逝慧,即此時狀態(tài)為 unlocked 或 biasable昔脯。
偏向鎖是對輕量級鎖的再優(yōu)化,嘗試在 acquire 和 release 中避免原子指令, 僅在第一次獲取時執(zhí)行一次原子指令笛臣,以將鎖定線程 ID 安裝到 mark word 中云稚。
但偏向鎖的適用范圍相對輕量級鎖來說更加有限,偏向鎖適用于單個進程反復(fù)獲取并釋放鎖沈堡,而其他進程很少訪問該鎖的情況静陈,即大多數(shù)對象在其生命周期中最多只能被一個線程鎖定的情況。
更多關(guān)于偏向鎖的知識诞丽,請見 Quickly Reacquirable Locks 和 Biased Locking in HotSpot
6.4 鎖的轉(zhuǎn)換
在 HotSpot JVM 中鲸拥,按照偏向鎖,輕量級鎖僧免,重量級鎖的順序來嘗試獲取對象的鎖刑赶。完整流程見下圖:
偏向鎖相關(guān)流程(對應(yīng)上圖中以1開頭的):
如果新分配的對象O是可偏向的但未被偏向(對應(yīng)上圖中1),那么第一次鎖定的時候使用 CAS 在 mark word 中插入線程T1的ID(對應(yīng)上圖中1-1)懂衩,而接下來的鎖定僅僅將 mark word 里面的 線程T1的ID 與 當(dāng)前線程T2的比較撞叨,此時可能出現(xiàn)兩種情況:
- 情況1:如果線程ID一樣,則表明對象O 已偏向當(dāng)前線程 T2浊洞,也就是當(dāng)前線程 T2 已經(jīng)鎖定對象 O牵敷,可以無需 CAS 即可 lock/unlock (對應(yīng)圖中1-2)
- 情況2:如果線程ID不一樣,則撤銷對T1的偏向, 并需要檢查對象O是否可以重偏向沛申。如果可以重偏向,則將對象O重偏向到線程T2(對應(yīng)上圖中1-3)姐军;否則將撤銷偏向并回退到正常鎖定流程(對應(yīng)上圖中以2開頭的)铁材,此后對象 O 對應(yīng)的類不可以再被偏向鎖定。
輕量級鎖和重量級鎖流程(對應(yīng)上圖中以2開頭的):
如果新分配的對象O對應(yīng)的類不可偏向奕锌,則先嘗試通過 CAS 設(shè)置 mark word來獲取輕量級鎖著觉。如果成功,則獲取輕量級鎖惊暴;如果失敗饼丘,則先判斷是否是遞歸鎖定,如果是則表明已經(jīng)獲取鎖辽话,如果不是則膨脹為重量級鎖肄鸽。
輕量級鎖定時卫病,每次進入同步方法,都會在棧幀中生成一個新的 lock record (鎖記錄)典徘,該鎖記錄有兩個字段displaced hdr和owner蟀苛,displaced hdr 用來保存鎖對象的對象頭mark word, owner用來保存指向鎖對象的指針逮诲。另外帜平,Lock record出于內(nèi)存對齊的要求,會確保lock record的存儲地址最后兩位為00 梅鹦,這兩位剛好用來作為輕量級鎖的標(biāo)識裆甩。
輕量級鎖定:嘗試將 lock record 的 displaced hdr 用來保存鎖對象原來的mark word;將lock record的owner指向鎖對象齐唆;將鎖對象原來的mark word 替換為指向lock record的指針嗤栓;這三步都會在同一個CAS原子地進行嘗試。
如果CAS 成功蝶念,則表明獲取輕量級鎖成功抛腕,也就是下圖所示的情況
如果CAS 失敗,則分為遞歸鎖定和需要膨脹到重量級鎖兩種情況處理媒殉。
虛擬機首先測試對象的 mark word 是否指向當(dāng)前線程的方法棧担敌。
- 如果是,則表明是遞歸鎖定廷蓉,當(dāng)前線程已經(jīng)擁有對象的鎖全封,可以安全地繼續(xù)執(zhí)行它。對于這種遞歸鎖定的對象桃犬,將 lock record 初始化為0而不是對象的 mark word刹悴。(對應(yīng)上圖中的2-2)
- 如果不是,則表明存在兩個不同的線程同時在同一個對象上同步攒暇,這時需要將輕量級鎖膨脹到重量級鎖土匀,也就是將指向heavy monitor的指針賦值給 對象的 mark word(對應(yīng)上圖中的2-3)
上面只是關(guān)于同步流程的部分總結(jié),關(guān)于同步更全面的介紹形用,請參見Sun 的 Eliminating Synchronization Related Atomic Operations with Biased Locking and Bulk Rebiasing 和 Synchronization in Java SE 6(HotSpot) 就轧,以及 Synchronization (我翻譯的Synchronization中英對照版 ,還有 Java虛擬機是怎么實現(xiàn)synchronized的田度? 妒御,以及在 bytecodeInterpreter.cpp 搜索Lock method if synchronized
。
7.總結(jié)
這篇文章首先對 synchronized 的基本使用進行了復(fù)習(xí)镇饺;然后嘗試從字節(jié)碼和本地機器碼的角度上看 synchronized 的實現(xiàn)乎莉;最后通過查看官方文檔弄清synchronized 的實現(xiàn)會分別嘗試偏向鎖(嘗試避免原子指令,僅第一次的時候需要使用原子指令,以將鎖定線程的 ID 安裝到 header word 中)惋啃,輕量級鎖(在鎖定和解鎖中使用 一個或兩個CPU-level 的原子指令)哼鬓,重量級鎖(操作系統(tǒng)級調(diào)用),這三種鎖實現(xiàn)適用范圍越來越大肥橙,但代價也越來越大魄宏。
其實 synchronized 如何實現(xiàn)對于一般人是無感的,這也是為什么每次 JDK 發(fā)布都可能會改善它的性能存筏,我們要做的基本上是根據(jù) JDK 版本理解對應(yīng)的實現(xiàn)宠互,然后調(diào)整一下 相應(yīng)的 JVM 參數(shù)。
8.參考
1.Java語言規(guī)范第八版第17章
2.Java虛擬機規(guī)范第八版
3.https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
4.https://time.geekbang.org/column/article/13530
5.https://blogs.oracle.com/dave/biased-locking-in-hotspot
6.https://wiki.openjdk.java.net/display/HotSpot/Synchronization
7.https://stackoverflow.com/questions/46312817/does-java-ever-rebias-an-individual-lock
8.https://www.zhihu.com/question/57774251