1. 程式人生 > >java併發系列二(深入!!!理解synchronized,volatile)

java併發系列二(深入!!!理解synchronized,volatile)

一,synchronized詳解

這個關鍵字大家想必是相當熟悉了,它是一個比較重量級的鎖,主要有兩層含義,一個是互斥性,一個是可見性。三種用法:1,修飾普通方法2,修飾靜態方法3,修飾程式碼塊
這裡有一點需要注意,普通方法要拿到當前例項的鎖,靜態方法要拿到當前class物件的鎖。
重點來了!!!
synchronized實現原理!!!
(這塊內容晦澀難懂,主要是參考的這篇博文https://blog.csdn.net/javazejian/article/details/72828483)

java虛擬機器中的同步是基於Monitor物件實現的。其中同步程式碼塊通過指令monitorenter和monitorexit來實現。同步方法是由方法呼叫指令讀取ACC_SYNCHRONIZED 標誌來實現的。
jvm中,物件在記憶體中的佈局如下
在這裡插入圖片描述

這裡只講一下物件頭:它是實現synchronized物件鎖的基礎。jvm中採用兩個字來儲存物件頭,MarkWord和Class Metadata Address。
MarkWord:儲存物件的hashCode和鎖資訊等
Class Metadata Address:型別指標指向物件的類元資料,JVM通過這個指標確定該物件是哪個類的例項。
其中Mark Word在預設情況下儲存著物件的HashCode、分代年齡、鎖標記位等。
重點!!重量級鎖的鎖標記位為10,指標指向的是monitor物件的起始地址。每個物件都有一個monitor與之關聯,當一個monitor被一個執行緒持有後,它就處於鎖定狀態。在java虛擬機器中,monitor是由ObjectMonitor實現的。其主要結構如下

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //記錄個數
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //處於wait狀態的執行緒,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //處於等待鎖block狀態的執行緒,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }
--------------------- 

ObjectMonitor有兩個佇列_WaitSet 和 _EntryList。當多個執行緒同時訪問一段程式碼時,首先會進入_EntryList。當執行緒獲取到物件的monitor 後進入 _Owner 區域並把monitor中的owner變數設定為當前執行緒同時monitor中的計數器count加1,若執行緒呼叫 wait() 方法,將釋放當前持有的monitor,owner變數恢復為null,count自減1,同時該執行緒進入 WaitSe t集合中等待被喚醒。若當前執行緒執行完畢也將釋放monitor(鎖)並復位變數的值,以便其他執行緒進入獲取monitor(鎖)。
在這裡插入圖片描述
接下來再簡單說一下同步程式碼塊的原理!!!(重要)
monitorenter指令指向同步程式碼塊的開始位置,monitorexit指令則指明同步程式碼塊的結束位置,當執行monitorenter指令時,當前執行緒將試圖獲取 objectref(即物件鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器為 0,那執行緒可以成功取得 monitor,並將計數器值設定為 1,取鎖成功。如果當前執行緒已經擁有 objectref 的 monitor 的持有權,那它可以重入這個 monitor重入時計數器的值也會加 1。倘若其他執行緒已經擁有 objectref 的 monitor 的所有權,那當前執行緒將被阻塞,直到正在執行執行緒執行完畢,即monitorexit指令被執行,執行執行緒將釋放 monitor(鎖)並設定計數器值為0 ,其他執行緒將有機會持有 monitor 。

 monitorenter  //進入同步方法
//..........省略其他  
 monitorexit   //退出同步方法

同步方法的原理
方法級的同步,無需通過位元組碼指令來控制。jvm可以從方法常量池的方法表結構中的 ACC_SYNCHRONIZED 訪問標誌區分一個方法是否同步方法。當方法呼叫時,呼叫指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設定,如果設定了,執行執行緒將先持有monitor(虛擬機器規範中用的是管程一詞), 然後再執行方法,最後再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執行期間,執行執行緒持有了monitor,其他任何執行緒都無法再獲得同一個monitor。如果一個同步方法執行期間拋 出了異常,並且在方法內部無法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放。

偏向鎖
如果一個執行緒執行緒獲得了鎖,那麼鎖就進入偏向模式,此時Mark Word的結構也變為偏向鎖結構。當這個執行緒再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個執行緒申請相同的鎖。但是對於鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的執行緒都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗後,並不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。

輕量級鎖
倘若偏向鎖失敗,虛擬機器並不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優化手段(1.6之後加入的),此時Mark Word 的結構也變為輕量級鎖的結構。輕量級鎖能夠提升程式效能的依據是“對絕大部分的鎖,在整個同步週期內都不存在競爭”。,輕量級鎖所適應的場景是執行緒交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。

自旋鎖
輕量級鎖失敗後,虛擬機器為了避免執行緒真實地在作業系統層面掛起,還會進行一項稱為自旋鎖的優化手段。這是基於在大多數情況下,執行緒持有鎖的時間都不會太長,如果直接掛起作業系統層面的執行緒可能會得不償失。
自旋鎖會假設在不久將來,當前的執行緒可以獲得鎖,因此虛擬機器會讓當前想要獲取鎖的執行緒做幾個空迴圈(這也是稱為自旋的原因),一般不會太久,可能是50個迴圈或100迴圈,在經過若干次迴圈後,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將執行緒在作業系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的。最後沒辦法也就只能升級為重量級鎖了。

二,volatile關鍵字詳解

這個關鍵字大家肯定也非常熟悉了,就是一種比synchronized關鍵字更輕量級的同步機制。讀取volatile型別的變數總會返回最新寫入的值。有一點需要注意,它不具有互斥性。
現在開始重點講原理
上圖
在這裡插入圖片描述
這個圖應該很好理解啦
這裡只講一下快取一致性的核心思想:當cpu寫資料時,如果發現該變數是共享變數,就會通知其他cpu將該變數的快取行設為無效。因此當其他cpu重新讀取這個變數時,就會發現自己的這個變數無效,就會從主記憶體中重新讀取。
下面這段話摘自《深入理解Java虛擬機器》:

“觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的彙編程式碼發現,加入volatile關鍵字時,會多出一個lock字首指令”

lock字首指令實際上相當於一個記憶體屏障(也成記憶體柵欄),記憶體屏障會提供3個功能:

1)它確保指令重排序時不會把其後面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的後面;即在執行到記憶體屏障這句指令時,在它前面的操作已經全部完成;

2)它會強制將對快取的修改操作立即寫入主存;

3)如果是寫操作,它會導致其他CPU中對應的快取行無效。