synchronized底層揭祕

 

前言

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

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

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

相信小夥伴們對於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專欄

消息中間件專欄

併發編程專欄

 

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