1. 程式人生 > >synchronized 原理分析

synchronized 原理分析

body list 依然 字節 繼續 方便 不同 副本 信息

synchronized 原理分析

1. synchronized 介紹

?? 在並發程序中,這個關鍵字可能是出現頻率最高的一個字段,他可以避免多線程中的安全問題,對代碼進行同步。同步的方式其實就是隱式的加鎖,加鎖過程是有 jvm 幫我們完成的,再生成的字節碼中會有體現,如果反編譯帶有不可消除的 synchronized 關鍵字的代碼塊的 class 文件我們會發現有兩個特殊的指令 monitorentermonitorexit ,這兩個就是進入管程和退出管程。為什麽說不可消除的 synchronized ,這是由於在編譯時期會進行鎖優化,比如說在 StringBuffer 中是加了鎖的,也就是鎖對象就是他自己,然而我們編譯以後會發現根本沒有上面的兩條指令就是因為,鎖消除技術。

?? Synchronized 使用的一般場景,在對象方法和類方法上使用,以及自定義同步代碼塊。但是在方法上使用 Synchronized 關鍵字和使用同步代碼塊是不一樣的,方法上采用同步是采用的字節碼中的標誌位 ACC_SYNCHRONIZED 來進行同步的。而同步代碼塊則是采用了對象頭中的鎖指針指向一個監視器(鎖),來完成同步。

?? 當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,如果設置了,執行線程將先獲取 monitor ,獲取成功之後才能執行方法體,方法執行完後再釋放 monitor 。在方法執行期間,其他任何線程都無法再獲得同一個 monitor 對象。 其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過字節碼來完成。

2. 對象頭和鎖

?? 一個對象在內存中分為三部分:對象頭、實例數據、對齊填充。

  1. 對象頭中主要存放了 GC 分代年齡、偏向鎖、偏向 id、鎖類型、hash 值等。jvm 一般會用兩個字來存放對象頭,(如果對象是數組則會分配3個字,多出來的1個字記錄的是數組長度),其主要結構是由Mark Word 和 Class Metadata Address 組成。MarkWord裏默認數據是存儲對象的HashCode等信息,但是會隨著對象的運行改變而發生變化,不同的鎖狀態對應著不同的記錄存儲方式
    技術分享圖片
  2. 實例數據就包括對象字段的值,不僅有自己的值還有繼承自父類的字段的值。一般字段的順序是同類型的字段放在一起,空間比較大的字段放在前面。在滿足上面的規則下父類的放在子類的前面。

  3. 對其填充並非必要的,整個對象需要是 8 字節的整數倍,當不足的時候會進行填充以達到 8 字節整數倍,主要還是為了方便存取。

?? 這裏我們主要分析一下重量級鎖也就是通常說synchronized的對象鎖,鎖標識位為10,其中指針指向的是monitor對象(在 Synchronized 代碼塊中的監視器 )的起始地址。每個對象都存在著一個 monitor 與之關聯,對象與其 monitor 之間的關系有存在多種實現方式,如 monitor 可以與對象一起創建銷毀或當線程試圖獲取對象鎖時自動生成,但當一個 monitor 被某個線程持有後,它便處於鎖定狀態。。在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的,其主要數據結構如下。

ObjectMonitor() {
    _count        = 0; //記錄個數
    _owner        = NULL; // 運行的線程
    //兩個隊列
    _WaitSet      = NULL; //調用 wait 方法會被加入到_WaitSet
   _EntryList    = NULL ; //鎖競爭失敗,會被加入到該列表
  }

?? ObjectMonitor中有兩個隊列,_WaitSet 和 _EntryList,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象),_owner指向持有ObjectMonitor對象的線程,當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的monitor 後進入 _Owner 區域並把monitor中的owner變量設置為當前線程同時monitor中的計數器count加1,若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復為null,count自減1,同時該線程進入 WaitSe t集合中等待被喚醒。若當前線程執行完畢也將釋放monitor(鎖)並復位變量的值,以便其他線程進入獲取monitor(鎖)。
技術分享圖片

3. Synchronized 代碼塊原理

反編譯下面的代碼得到的字節碼如下:

public class SynchronizedTest {
    public static void main(String[] args) {
        synchronized (SynchronizedTest.class) {
            System.out.println("hello");
        }
    }

    public synchronized void test(){

    }
}

技術分享圖片

?? 當執行monitorenter指令時,當前線程將試圖獲取 objectref(即對象鎖) 所對應的 monitor 的持有權,當 objectref 的 monitor 的進入計數器為 0,那線程可以成功取得 monitor,並將計數器值設置為 1,取鎖成功。如果當前線程已經擁有 objectref 的 monitor 的持有權,那它可以重入這個 monitor ,重入時計數器的值也會加 1。倘若其他線程已經擁有 objectref 的 monitor 的所有權,那當前線程將被阻塞,直到正在執行線程執行完畢,即monitorexit指令被執行,執行線程將釋放 monitor(鎖)並設置計數器值為0 ,其他線程將有機會持有 monitor 。值得註意的是編譯器將會確保無論方法通過何種方式完成,方法中調用過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而無論這個方法是正常結束還是異常結束。為了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理所有的異常,它的目的就是用來執行 monitorexit 指令。所以看到上面有兩條 monitorexit !

4. Synchronized 方法原理

?? 先看一個反編譯的實例方法的結果,確實比普通的方法多了一個標誌字段。方法級的同步是隱式,即無需通過字節碼指令來控制的,它實現在方法調用和返回操作之中。當方法調用時,調用指令將會 檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,如果設置了,執行線程將先持有 monitor , 然後再執行方法,最後再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執行期間,執行線程持有了monitor,其他任何線程都無法再獲得同一個monitor。如果一個同步方法執行期間拋 出了異常,並且在方法內部無法處理此異常,那這個同步方法所持有的monitor將在異常拋到同步方法之外時自動釋放。
技術分享圖片

5. 偏向鎖

?? 偏向鎖是 Java 為了提高程序的性能而設計的一個比較優雅的加鎖方式。偏向鎖的核心思想是,如果一個線程獲得了鎖,那麽鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個線程再次請求鎖時,無需再做獲取鎖的過程。如果有其他線程競爭鎖的時候就需要膨脹為輕量級鎖。這樣就省去了大量有關鎖申請的操作,從而也就提供程序的性能。

?? 所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個線程申請相同的鎖。但是對於鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要註意的是,偏向鎖失敗後,並不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。

?? 偏向鎖獲取的過程如下,當鎖對象第一次被線程獲取的時候,虛擬機把對象頭中的標誌位設為“01”,即偏向模式。同時使用CAS操作把獲取到這個鎖的線程的ID記錄在對象的Mark Word之中的偏向線程ID,並將是否偏向鎖的狀態位置置為1。如果CAS操作成功,持有偏向鎖的線程以後每次進入這個鎖相關的同步塊時,直接檢查ThreadId是否和自身線程Id一致,
如果一致,則認為當前線程已經獲取了鎖,虛擬機就可以不再進行任何同步操作(例如Locking、Unlocking及對Mark Word的Update等)。

?? 其實一般來說偏向鎖很少又說去主動釋放的,因為只有在其他線程需要獲取鎖的時候,也就是這個鎖不僅僅被一個線程使用,可能有兩個線程交替使用,根據對象是否被鎖定來決定釋放鎖(恢復到未鎖定狀態)還是升級到輕量鎖狀態。

6.輕量級鎖

?? 輕量級鎖,一般指的是在有兩個線程在交替使用鎖的時候由於沒有同時搶鎖屬於一種比較和諧的狀態,就可以使用輕量級鎖。他的基本思想是,當線程要獲取鎖時把鎖對象的 Mark Word 復制一份到當前線程的棧頂,然後執行一個 CAS 操作把鎖對象的 Mark Word 更新為指向棧頂的副本的指針,如果成功則當前線程擁有了鎖。可以進行同步代碼塊的執行,而失敗則有兩種可能,要麽是當前線程已經擁有了鎖對象的指針,這時可以繼續執行。要麽是被其他線程搶占了鎖對象,這時候說明了在同一時間有兩個線程同時需要競爭鎖,那麽就打破了這種和諧的局面需要膨脹到重量級鎖,鎖對象的標誌修改,獲取線程的鎖等待。
?? 在輕量級鎖釋放的過程就采用 CAS 把棧上的賦值的 Mark Word 替換到鎖對象上,如果失敗說明有其他線程執搶占過鎖,鎖對象的 Mark Word 的標誌被修改過,在釋放的同時喚醒等待的線程。

synchronized 原理分析