synchronized底層揭祕

王子發表於2020-11-30

 

前言

上篇文章我們從硬體級別探索,對可見性和有序性的認識上升了一個高度,卻遲遲沒有介紹原子性的解決方案。

今天我們就來聊一聊原子性的解決方案,

引入鎖機制,除了可以保證原子性,同時也可以保證可見性和有序性。

相信小夥伴們對於synchronized互斥鎖一定很熟悉,但是你懂它的實現原理嗎,今天就讓我們一起來揭開它的神祕面紗吧。

 

synchronized的原子性

首先我們來看一下synchronized是怎麼保證原子性的。

其實往最簡單瞭解釋,還是比較容易理解的。synchronized加鎖主要靠的是monitor,monitor在java裡可以理解成一個監視器,在作業系統裡它又被稱為管程。

簡單的模型如下圖:

當我們的程式通過synchronized鎖定一個物件的時候,這個物件會關聯一個monitor,獲取鎖時會對monitor中的計數器進行+1操作,釋放鎖的時候進行-1操作,同時也是支援可重入的,同一個執行緒再次獲取該物件的鎖,計數器就再+1。

如果計數器為0就代表完全釋放了鎖,其他執行緒可以獲取鎖。

如果執行緒呼叫了wait方法,會釋放鎖資源,同時把執行緒放入waitset中,等待notifyall方法喚醒,喚醒後重新開始競爭鎖資源。

這就是sychronized鎖的最簡單的解釋,我們當然不會滿足於此,接下來我們繼續深入研究一下

先看一段程式碼:

MyLock lock = new MyLock();//一個自定義的鎖物件
sychronized(lock){
//...
}

java的物件在記憶體中儲存的佈局可以分為三塊區域:物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)

物件頭包含Mark Word(包含hashcode、鎖資料、GC資訊等)和Class MetaData Address(指向Class物件的指標)。

例項資料就是我們在物件裡存放的那些資料。

java要求物件大小為8位元組的整數倍,對齊填充就是用來填充位元組的,沒有其他意義。

Mark Word會指向一個monitor,這個monitor是C++實現的一個Object Monitor物件,首先執行緒在獲取鎖時,先進入到entry list中,然後通過CAS對count計數器進行+1操作,如果+1成功代表獲取到鎖,此時就會把該執行緒的資訊放入owner中,owner就是用來儲存當前獲取到鎖的執行緒的。整體結構如圖所示:

 

 

 

 

sychronized的可見性

在說可見性之前,我們先引入兩個概念:Store屏障和Load屏障

Store屏障就是強制CPU執行flush操作,Load屏障就是強制CPU執行refresh操作。

flush和refresh我們上篇文章已經說過,這裡就不再解釋了。

那sychronized是如何實現可見性的呢,其實就是利用了記憶體屏障。如下:

sychronized(this){
   // monitorenter
   // Load記憶體屏障
   //...  
}
//monitorexit
//Store記憶體屏障

 

sychronized的有序性

同樣在說有序性之前引入兩個新的記憶體屏障:Acquire屏障和Release屏障

Acquire屏障可以禁止讀操作和其他讀寫操作之間發生指令重排,Release屏障可以禁止寫操作和其他讀寫操作之間發生指令重排。

那sychronized是如何實現有序性的呢,其實就是利用了這兩個記憶體屏障。如下:

sychronized(this){
   // monitorenter
   // Load記憶體屏障
   // Acquire記憶體屏障
   //...  
   //Release記憶體屏障
}
 //monitorexit
 //Store記憶體屏障

需要注意的是Acquire屏障和Release屏障保證的是sychronized內部的程式碼不會與外部的程式碼之間發生指令重排,內部的程式碼自己還是可能發生指令重排的。

 

sychronized的鎖優化

jdk1.6後jvm對sychronized進行了鎖優化,這部分我們做個概念瞭解就可以了。

1.鎖消除

鎖消除是JIT編譯器對sychronized的優化,在編譯的時候會通過逃逸分析技術,來分析鎖物件。如果只有一個執行緒來加鎖和解鎖,沒有鎖競爭,那就沒有必要加鎖,會去掉monitorenter和monitorexit指令。

2.鎖粗化

這個意思是,如果有多個連續的加鎖釋放鎖操作,那麼編譯後會變成一把鎖。

例如

sychronized(this){}

sychronized(this){}

sychronized(this){}

連著三個加鎖操作,編譯後會變成一個。

3.偏向鎖

偏向鎖主要是為了減少monitorenter和monitorexit指令的CAS操作,減少開銷,如果認為當前鎖大概率只有一個執行緒來競爭,那麼就會給這個鎖維護好一個偏好Bias,之後該執行緒加鎖和釋放鎖都通過這個Bias來執行,不需要去執行CAS了。

但是如果發現有其他執行緒來競爭鎖,就會收回之前分配好的偏好。

4.輕量級鎖

如果偏向鎖沒能實現,也就是說有多個執行緒競爭鎖,那麼就會採用輕量級鎖。

其實就是將物件裡的輕量級鎖指標指向一個已經獲取了鎖的執行緒,然後判斷一下是不是自己加的鎖,如果是就直接執行,如果不是說明有其他執行緒加了鎖,就會升級為重量級鎖,重量級鎖流程我們上文中介紹原子性的時候已經說過了。

5.適應性自旋鎖

在許多場景中,同步資源的鎖定時間很短,為了這一小段時間去切換執行緒,執行緒掛起和恢復的花費可能會讓系統得不償失。為了讓當前執行緒“稍等一下”,我們需讓當前執行緒進行自旋。

如果在自旋完成後前面鎖同步資源的執行緒已經釋放了鎖,那麼當前執行緒就可以不必阻塞而是直接獲取同步資源,從而避免了切換執行緒的開銷。這就是自旋鎖。

適應性自旋鎖意味著自旋的時間(次數)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。

 

總結

到這裡,有關synchronized的底層實現我們基本上已經聊完了。

使用鎖來保證原子性,使用記憶體屏障來保證可見性和有序性。

同時jvm又對sychronized做了一些優化。

相信小夥伴們理解了本文的內容,會收穫頗豐。

那我們下次再見。

 

往期文章推薦:

JVM專欄

訊息中介軟體專欄

併發程式設計專欄

 

相關文章