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