前言
上篇文章我們從硬體級別探索,對可見性和有序性的認識上升了一個高度,卻遲遲沒有介紹原子性的解決方案。
今天我們就來聊一聊原子性的解決方案,鎖。
引入鎖機制,除了可以保證原子性,同時也可以保證可見性和有序性。
相信小夥伴們對於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做了一些優化。
相信小夥伴們理解了本文的內容,會收穫頗豐。
那我們下次再見。
往期文章推薦: