synchronized 原理分析

synchronized 原理分析

1. synchronized 介紹

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

   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實現的,其主要數據結構以下。jvm

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 的標誌被修改過,在釋放的同時喚醒等待的線程。

相關文章
相關標籤/搜索