1. 程式人生 > >Synchronized解析——如果你願意一層一層剝開我的心

Synchronized解析——如果你願意一層一層剝開我的心

前言

synchronized,是解決併發情況下資料同步訪問問題的一把利刃。那麼synchronized的底層原理是什麼呢?下面我們來一層一層剝開它的心,就像剝洋蔥一樣,看個究竟。

Synchronized的使用場景

synchronized關鍵字可以作用於方法或者程式碼塊,最主要有以下幾種使用方式,如圖:

接下來,我們先剝開synchronized的第一層,反編譯其作用的程式碼塊以及方法。

synchronized作用於程式碼塊

public class SynchronizedTest {

    public void doSth(){
        synchronized (SynchronizedTest.class){
            System.out.println("test Synchronized" );
        }
    }
}

反編譯,可得:

由圖可得,添加了synchronized關鍵字的程式碼塊,多了兩個指令monitorenter、monitorexit。即JVM使用monitorenter和monitorexit兩個指令實現同步,monitorenter、monitorexit又是怎樣保證同步的呢?我們等下剝第二層繼續探索。

synchronized作用於方法

 public synchronized void doSth(){
            System.out.println("test Synchronized method" );
    }

反編譯,可得:

由圖可得,添加了synchronized關鍵字的方法,多了ACC_SYNCHRONIZED標記。即JVM通過在方法訪問識別符號(flags)中加入ACC_SYNCHRONIZED來實現同步功能。

monitorenter、monitorexit、ACC_SYNCHRONIZED

剝完第一層,反編譯synchronized的方法以及程式碼塊,我們已經知道synchronized是通過monitorenter、monitorexit、ACC_SYNCHRONIZED實現同步的,它們三作用都是啥呢?我們接著剝第二層:

monitorenter

monitorenter指令介紹

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.

If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

谷歌翻譯一下,如下:

每個物件都與一個monitor 相關聯。當且僅當擁有所有者時(被擁有),monitor才會被鎖定。執行到monitorenter指令的執行緒,會嘗試去獲得對應的monitor,如下:

每個物件維護著一個記錄著被鎖次數的計數器, 物件未被鎖定時,該計數器為0。執行緒進入monitor(執行monitorenter指令)時,會把計數器設定為1.

當同一個執行緒再次獲得該物件的鎖的時候,計數器再次自增.

當其他執行緒想獲得該monitor的時候,就會阻塞,直到計數器為0才能成功。

可以看一下以下的圖,便於理解用:

monitorexit

monitorexit指令介紹

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.

The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

谷歌翻譯一下,如下:

monitor的擁有者執行緒才能執行 monitorexit指令。

執行緒執行monitorexit指令,就會讓monitor的計數器減一。如果計數器為0,表明該執行緒不再擁有monitor。其他執行緒就允許嘗試去獲得該monitor了。

可以看一下以下的圖,便於理解用:

ACC_SYNCHRONIZED

ACC_SYNCHRONIZED介紹

Method-level synchronization is performed implicitly, as part of method invocation and return. A synchronized method is distinguished in the run-time constant pool’s method_info structure by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.

谷歌翻譯一下,如下:

方法級別的同步是隱式的,作為方法呼叫的一部分。同步方法的常量池中會有一個ACC_SYNCHRONIZED標誌。

當呼叫一個設定了ACC_SYNCHRONIZED標誌的方法,執行執行緒需要先獲得monitor鎖,然後開始執行方法,方法執行之後再釋放monitor鎖,當方法不管是正常return還是丟擲異常都會釋放對應的monitor鎖。

在這期間,如果其他執行緒來請求執行方法,會因為無法獲得監視器鎖而被阻斷住。

如果在方法執行過程中,發生了異常,並且方法內部並沒有處理該異常,那麼在異常被拋到方法外面之前監視器鎖會被自動釋放。

可以看一下這個流程圖:

Synchronized第二層的總結

  • 同步程式碼塊是通過monitorenter和monitorexit來實現,當執行緒執行到monitorenter的時候要先獲得monitor鎖,才能執行後面的方法。當執行緒執行到monitorexit的時候則要釋放鎖。
  • 同步方法是通過中設定ACC_SYNCHRONIZED標誌來實現,當執行緒執行有ACC_SYNCHRONI標誌的方法,需要獲得monitor鎖。
  • 每個物件維護一個加鎖計數器,為0表示可以被其他執行緒獲得鎖,不為0時,只有當前鎖的執行緒才能再次獲得鎖。
  • 同步方法和同步程式碼塊底層都是通過monitor來實現同步的。
  • 每個物件都與一個monitor相關聯,執行緒可以佔有或者釋放monitor。

好的,剝到這裡,我們還有一些不清楚的地方,monitor是什麼呢,為什麼它可以實現同步呢?物件又是怎樣跟monitor關聯的呢?客觀別急,我們繼續剝下一層,請往下看。

monitor監視器

montor到底是什麼呢?我們接下來剝開Synchronized的第三層,monitor是什麼? 它可以理解為一種同步工具,或者說是同步機制,它通常被描述成一個物件。作業系統的管程是概念原理,ObjectMonitor是它的原理實現。

作業系統的管程

  • 管程 (英語:Monitors,也稱為監視器) 是一種程式結構,結構內的多個子程式(物件或模組)形成的多個工作執行緒互斥訪問共享資源。
  • 這些共享資源一般是硬體裝置或一群變數。管程實現了在一個時間點,最多隻有一個執行緒在執行管程的某個子程式。
  • 與那些通過修改資料結構實現互斥訪問的併發程式設計相比,管程實現很大程度上簡化了程式設計。
  • 管程提供了一種機制,執行緒可以臨時放棄互斥訪問,等待某些條件得到滿足後,重新獲得執行權恢復它的互斥訪問。

ObjectMonitor

ObjectMonitor資料結構

在Java虛擬機器(HotSpot)中,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關鍵字

ObjectMonitor中幾個關鍵欄位的含義如圖所示:

工作機理

Java Monitor 的工作機理如圖所示:

  • 想要獲取monitor的執行緒,首先會進入_EntryList佇列。
  • 當某個執行緒獲取到物件的monitor後,進入_Owner區域,設定為當前執行緒,同時計數器_count加1。
  • 如果執行緒呼叫了wait()方法,則會進入_WaitSet佇列。它會釋放monitor鎖,即將_owner賦值為null,_count自減1,進入_WaitSet佇列阻塞等待。
  • 如果其他執行緒呼叫 notify() / notifyAll() ,會喚醒_WaitSet中的某個執行緒,該執行緒再次嘗試獲取monitor鎖,成功即進入_Owner區域。
  • 同步方法執行完畢了,執行緒退出臨界區,會將monitor的owner設為null,並釋放監視鎖。

為了形象生動一點,舉個例子:

  synchronized(this){  //進入_EntryList佇列
            doSth();
            this.wait();  //進入_WaitSet佇列
        }

OK,我們又剝開一層,知道了monitor是什麼了,那麼物件又是怎樣跟monitor關聯呢?各位帥哥美女們,我們接著往下看,去剝下一層。

物件與monitor關聯

物件是如何跟monitor關聯的呢?直接先看圖:

看完上圖,其實物件跟monitor怎樣關聯,我們已經有個大概認識了,接下來我們分物件記憶體佈局,物件頭,MarkWord一層層繼續往下探討。

物件的記憶體佈局

在HotSpot虛擬機器中,物件在記憶體中儲存的佈局可以分為3塊區域:物件頭(Header),例項資料(Instance Data)和物件填充(Padding)。

  • 例項資料:物件真正儲存的有效資訊,存放類的屬性資料資訊,包括父類的屬性資訊;
  • 對齊填充:由於虛擬機器要求 物件起始地址必須是8位元組的整數倍。填充資料不是必須存在的,僅僅是為了位元組對齊。
  • 物件頭:Hotspot虛擬機器的物件頭主要包括兩部分資料:Mark Word(標記欄位)、Class Pointer(型別指標)。

物件頭

物件頭主要包括兩部分資料:Mark Word(標記欄位)、Class Pointer(型別指標)。

  • Class Pointer:是物件指向它的類元資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項
  • Mark Word : 用於儲存物件自身的執行時資料,它是實現輕量級鎖和偏向鎖的關鍵。

Mark word

Mark Word 用於儲存物件自身的執行時資料,如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒 ID、偏向時間戳等。

在32位的HotSpot虛擬機器中,如果物件處於未被鎖定的狀態下,那麼Mark Word的32bit空間裡的25位用於儲存物件雜湊碼,4bit用於儲存物件分代年齡,2bit用於儲存鎖標誌位,1bit固定為0,表示非偏向鎖。其他狀態如下圖所示:

  • 前面分析可知,monitor特點是互斥進行,你再喵一下上圖,重量級鎖,指向互斥量的指標。
  • 其實synchronized是重量級鎖,也就是說Synchronized的物件鎖,Mark Word鎖標識位為10,其中指標指向的是Monitor物件的起始地址。
  • 頓時,是不是感覺柳暗花明又一村啦!物件與monitor怎麼關聯的?答案:Mark Word重量級鎖,指標指向monitor地址。

Synchronized剝開第四層小總結

物件與monitor怎麼關聯?

  • 物件裡有物件頭
  • 物件頭裡面有Mark Word
  • Mark Word指標指向了monitor

鎖優化

事實上,只有在JDK1.6之前,synchronized的實現才會直接呼叫ObjectMonitor的enter和exit,這種鎖被稱之為重量級鎖。一個重量級鎖,為啥還要經常使用它呢? 從JDK6開始,HotSpot虛擬機器開發團隊對Java中的鎖進行優化,如增加了適應性自旋、鎖消除、鎖粗化、輕量級鎖和偏向鎖等優化策略。

自旋鎖

何為自旋鎖?

自旋鎖是指當一個執行緒嘗試獲取某個鎖時,如果該鎖已被其他執行緒佔用,就一直迴圈檢測鎖是否被釋放,而不是進入執行緒掛起或睡眠狀態。

為何需要自旋鎖?

執行緒的阻塞和喚醒需要CPU從使用者態轉為核心態,頻繁的阻塞和喚醒顯然對CPU來說苦不吭言。其實很多時候,鎖狀態只持續很短一段時間,為了這段短暫的光陰,頻繁去阻塞和喚醒執行緒肯定不值得。因此自旋鎖應運而生。

自旋鎖應用場景

自旋鎖適用於鎖保護的臨界區很小的情況,臨界區很小的話,鎖佔用的時間就很短。

自旋鎖一些思考

在這裡,我想談談,為什麼ConcurrentHashMap放棄分段鎖,而使用CAS自旋方式,其實也是這個道理。

鎖消除

何為鎖消除?

鎖削除是指虛擬機器即時編譯器在執行時,對一些程式碼上要求同步,但是被檢測到不可能存在共享資料競爭的鎖進行削除。

鎖消除一些思考

在這裡,我想引申到日常程式碼開發中,有一些開發者,在沒併發情況下,也使用加鎖。如沒併發可能,直接上來就ConcurrentHashMap。

鎖粗化

何為鎖租化?

鎖粗話概念比較好理解,就是將多個連續的加鎖、解鎖操作連線在一起,擴充套件成一個範圍更大的鎖。

為何需要鎖租化?

在使用同步鎖的時候,需要讓同步塊的作用範圍儘可能小—僅在共享資料的實際作用域中才進行同步,這樣做的目的是 為了使需要同步的運算元量儘可能縮小,如果存在鎖競爭,那麼等待鎖的執行緒也能儘快拿到鎖。但是如果一系列的連續加鎖解鎖操作,可能會導致不必要的效能損耗,所以引入鎖粗話的概念。

鎖租化比喻思考

舉個例子,買門票進動物園。老師帶一群小朋友去參觀,驗票員如果知道他們是個集體,就可以把他們看成一個整體(鎖租化),一次性驗票過,而不需要一個個找他們驗票。

總結

我們直接以一張Synchronized洋蔥圖作為總結吧,如果你願意一層一層剝開我的心。

參考與感謝

  • Synchronized之管程 https://www.jianshu.com/p/32e1361817f0
  • 深入理解多執行緒(一)——Synchronized的實現原理 https://www.hollischuang.com/archives/1883
  • 深入理解多執行緒(五)—— Java虛擬機器的鎖優化技術 https://www.hollischuang.com/archives/2344
  • 《深入理解Java虛擬機器》

個人公眾號

歡迎大家關注,大家一起學習,一起討論哈