深入理解Java中的鎖

tony0087發表於2021-09-09

Java中的鎖

常見的鎖有synchronized、volatile、偏向鎖、輕量級鎖、重量級鎖

1、synchronized

  • synchronized是併發程式設計中接觸的最基本的同步工具,是一種重量級鎖,也是java內建的同步機制,首先我們知道synchronized提供了互斥性的語義和可見性,那麼我們可以透過使用它來保證併發的安全。

  • synchronized三種用法:

    • synchronized與其他鎖不同,它是內建在JVM中的,從JVM規範中看,JVM基於進入和退出Monitor物件來實現方法同步和程式碼塊同步,但兩者的實現細節不一樣。程式碼塊同步是使用monitorenter和monitorexit指令實現的,而方法同步是使用另外一種方式實現的。monitorenter指令是在編譯後插入到同步程式碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何物件都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態。執行緒執行到monitorenter指令時,將會嘗試獲取物件所對應的monitor的所有權,即嘗試獲得物件的鎖。

    • 方法級的同步是隱式的, 即無須透過位元組碼指令來控制, 它實現在方法呼叫和返回操作之中。 虛擬機器可以從方法常量池的方法表結構中的ACC_SYNCHRONIZED訪問標誌得知一個方法是否宣告為同步方法。 當方法呼叫時, 呼叫指令將會檢查方法的ACC_SYNCHRONIZED訪問標誌是否被設定, 如果設定了, 執行執行緒就要求先成功持有管程, 然後才能執行方法, 最後當方法完成(無論是正常完成還是非正常完成) 時釋放管程。 在方法執行期間, 執行執行緒持有了管程, 其他任何執行緒都無法再獲取到同一個管程。 如果一個同步方法執行期間丟擲了異常, 並且在方法內部無法處理此異常, 那麼這個同步方法所持有的管程將在異常拋到同步方法之外時自動釋放。

    • 當使用synchronized修飾程式碼塊時,那麼當前加鎖的級別就是synchronized(X)中配置的x物件例項,當多個執行緒併發訪問該物件的同步方法、同步程式碼塊以及當前的程式碼塊時,會進行同步。

    • 使用同步程式碼塊時要注意的是不要使用String型別物件,因為String常量池的存在,所以很容易導致出問題。

    • 當使用synchronized修飾類靜態方法時,那麼當前加鎖的級別就是類,當多個執行緒併發訪問該類(所有例項物件)的同步方法以及同步程式碼塊時,會進行同步。

    • 當使用synchronized修飾類普通方法時,那麼當前加鎖的級別就是例項物件,當多個執行緒併發訪問該物件的同步方法、同步程式碼塊時,會進行同步。

    • 物件鎖

    • 類鎖

    • 同步程式碼塊

    • synchronized實現原理

  • 實踐

    圖片描述

    image

    test()的位元組碼標註如下

    圖片描述

    image

2、volatile

  • 可見性

    • 我們從jmm的角度來看一下,每個執行緒擁有自己的工作記憶體,實際上執行緒所修改的共享變數是從主記憶體中複製的副本,當一個共享變數被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他執行緒需要讀取時,它會去記憶體中讀取新值。

    • 我們知道volatile可以看做是一種synchronized的輕量級鎖,他能夠保證併發時,被它修飾的共享變數的可見性,那麼他是如何實現可見性的呢?

  • 實現原理

    • 1、將當前處理器快取行的資料寫回到系統記憶體。

    • 2、這個寫回記憶體的操作會使在其他CPU裡快取了該記憶體地址的資料無效。
      為了提高處理速度,處理器不直接和記憶體進行通訊,而是先將系統記憶體的資料讀到內部快取(L1,L2或其他)後再進行操作,但操作完不知道何時會寫到記憶體。如果對宣告瞭volatile的變數進行寫操作,JVM就會向處理器傳送一條Lock字首的指令,將這個變數所在快取行的資料寫回到系統記憶體。但是,就算寫回到記憶體,如果其他處理器快取的值還是舊的,再執行計算操作就會有問題。所以,在多處理器下,為了保證各個處理器的快取是一致的,就會實現快取一致性協議,每個處理器透過嗅探在匯流排上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定成無效狀態,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器快取裡

    • 被volatile修飾的共享變數在進行寫操作的時候:

  • 使用場景

    • 1.訪問變數不需要加鎖(加鎖的話使用volatile就沒必要了)

    • 2、對變數的寫操作不依賴於當前值(因為他不能保證原子性)

    • 3.該變數沒有包含在具有其他變數的不變式中。

綜上所述:一般我們會用來修飾狀態標誌;讀寫鎖(讀>>寫,對寫加鎖,讀不加鎖);DCL的單例模式中;volatile bean(例如放入HTTPSession中的物件)

瞭解完上面的知識,我們來做一下對比:

- **相同點**:都保證了可見性
- **不同點** : volatile不能保證原子性,但是synchronized會發生阻塞(線上程狀態轉換中詳說),開銷更大。

3、問題的引入

  • 實際上我們知道鎖的本質就是執行緒等待,可以分為執行緒阻塞和執行緒自旋,關於他們的區別:

    從java鎖的型別來說,阻塞對應的就是悲觀鎖,自旋對應的就是樂觀鎖。在java中樂觀鎖主要的實現方式就是CAS操作,我們來簡單說一下CAS。

    CAS:一個CAS方法包含三個引數CAS(V,E,N)。V表示要更新的變數,E表示預期的值,N表示新值。只有當V的值等於E時,才會將V的值修改為N。如果V的值不等於E,說明已經被其他執行緒修改了,當前執行緒可以放棄此操作,也可以再次嘗試次操作直至修改成功。基於這樣的演算法,CAS操作即使沒有鎖,也可以發現其他執行緒對當前執行緒的干擾(臨界區值的修改),並進行恰當的處理。

    • 阻塞:要阻塞或喚醒一個執行緒就需要作業系統介入,需要在戶態與核心態之間切換,這種切換會消耗大量的系統資源。 如果執行緒狀態切換是一個高頻操作時,這將會消耗很多CPU處理時間, 如果對於那些需要同步的簡單的程式碼塊,獲取鎖掛起操作消耗的時間比使用者程式碼執行的時間還要長,這種同步策略顯然非常糟糕的。

    • 自旋:如果持有鎖的執行緒能在很短時間內釋放鎖資源,那麼那些等待競爭鎖的執行緒就不需要做核心態和使用者態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的執行緒釋放鎖後即可立即獲取鎖,這樣就避免使用者執行緒和核心的切換的消耗。(執行緒還是Runnable的,只是在執行空程式碼。當然一直自旋也會白白消耗計算資源。)

  • 額外引申技術點:volatile

    • 上面說到當前執行緒可以發現其他執行緒對臨界區資料的修改,這點可以使用volatile進行保證。volatile實現了JMM中的可見性。使得對臨界區資源的修改可以馬上被其他執行緒看到。

  • synchronized用的鎖是存在Java物件頭裡的。如果物件是陣列型別,則虛擬機器用3個字寬
    (Word)儲存物件頭(也就是24個位元組),如果物件是非陣列型別,則用2字寬儲存物件頭(16個位元組)。在64位虛擬機器中,1字寬等於8位元組,即64bit。

    圖片描述

    markword資料的長度在32位和64位的虛擬機器(未開啟壓縮指標)中分別為32bit和64bit,它的最後2bit是鎖狀態標誌位,用來標記當前物件的狀態,物件的所處的狀態,決定了markword儲存的內容(它會根據物件的狀態複用自己的儲存空間)

    64位虛擬機器在不同狀態下markword結構如下圖所示:JVM原始碼中是這麼寫的

    圖片描述

    最後2bit是鎖狀態標誌位,用來標記當前物件的狀態,物件的所處的狀態,決定了markword儲存的內容,如下表所示:

    圖片描述

3、自旋鎖

  • 自旋鎖原理非常簡單,如果持有鎖的執行緒能在很短時間內釋放鎖資源,那麼那些等待競爭鎖的執行緒就不需要做核心態和使用者態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的執行緒釋放鎖後即可立即獲取鎖,這樣就避免使用者執行緒和核心的切換的消耗。

  • 但是執行緒自旋是需要消耗cup的,說白了就是讓cup在做無用功,執行緒不能一直佔用cup自旋做無用功,所以需要設定一個自旋等待的最大時間。如果持有鎖的執行緒執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的執行緒在最大等待時間內還是獲取不到鎖,這時爭用執行緒會停止自旋進入阻塞狀態。

  • 在JDK 1.6中引入了自適應的自旋鎖。 自適應意味著自旋的時間不再固定了, 而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。 如果在同一個鎖物件上, 自旋等待剛剛成功獲得過鎖, 並且持有鎖的執行緒正在執行中, 那麼虛擬機器就會認為這次自旋也很有可能再次成功, 進而它將允許自旋等待持續相對更長的時間, 比如100個迴圈。 另外, 如果對於某個鎖, 自旋很少成功獲得過, 那在以後要獲取這個鎖時將可能省略掉自旋過程, 以避免浪費處理器資源。

4、偏向鎖

  • 偏向鎖(顧名思義,它會偏向於第一個訪問鎖的執行緒,如果在執行過程中,同步鎖只有一個執行緒訪問,不存在多執行緒爭用的情況,則執行緒是不需要觸發同步的,這種情況下,就會給執行緒加一個偏向鎖)

  • 大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了偏向鎖。

  • 偏向鎖的獲取

    • 以後該執行緒在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下物件頭的Mark Word裡是否儲存著指向當前執行緒的偏向鎖。如果測試成功,表示執行緒已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設定成1(表示當前是偏向鎖):如果沒有設定,則使用CAS競爭鎖;如果設定了,則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒。

    • 1、訪問Mark Word中偏向鎖的標識是否設定成1,鎖標誌位是否為01——確認為可偏向狀態。

    • 2、如果為可偏向狀態,則測試執行緒ID是否指向當前執行緒,如果是,進入步驟(5),否則進入步驟(3)。

    • 3、如果執行緒ID並未指向當前執行緒,則透過CAS操作競爭鎖。如果競爭成功,則將Mark Word中執行緒ID設定為當前執行緒ID,然後執行(5);如果競爭失敗,執行(4)。

    • 4、如果CAS獲取偏向鎖失敗,則表示有競爭。當到達全域性安全點(safepoint)時獲得偏向鎖的執行緒被掛起,偏向鎖升級為輕量級鎖,然後被阻塞在安全點的執行緒繼續往下執行同步程式碼。

    • 5、執行同步程式碼。

  • 偏向鎖的撤銷

    • 1、當有另外的執行緒檢視鎖定某個已經被偏向過得物件,jvm就需要撤銷偏向鎖。執行緒不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全域性安全點(在這個時間點上沒有位元組碼正在執行),它會首先暫停擁有偏向鎖的執行緒,判斷鎖物件是否處於被鎖定狀態,撤銷偏向鎖後恢復到未鎖定(標誌位為“01”)或輕量級鎖(標誌位為“00”)的狀態。

5、輕量級鎖

  • 輕量級鎖是由偏向所升級來的,偏向鎖執行在一個執行緒進入同步塊的情況下,當第二個執行緒加入鎖爭用的時候,偏向鎖就會升級為輕量級鎖;

  • 在《深入理解java虛擬機器中》是這樣說的,如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量, 那偏向鎖就是在無競爭的情況下把整個同步都消除掉, 連CAS操作都不做了。

6、重量級鎖Synchronized

前邊已經介紹了各種鎖,下邊主要介紹它們之間的關係

圖片描述



作者:AKyS佐毅
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/1747/viewspace-2818711/,如需轉載,請註明出處,否則將追究法律責任。

相關文章