天天看點

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

前言

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

Synchronized的使用場景

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

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

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

synchronized作用于代碼塊

public

class

SynchronizedTest

{

public

void
 doSth
(){

synchronized

(
SynchronizedTest
.
class
){

System
.
out
.
println
(
"test Synchronized"

);

}

}
}           

反編譯,可得:

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

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

synchronized作用于方法

public

synchronized

void
 doSth
(){

System
.
out
.
println
(
"test Synchronized method"

);

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

由圖可得,添加了synchronized關鍵字的方法,多了ACCSYNCHRONIZED标記。即JVM通過在方法通路辨別符(flags)中加入ACCSYNCHRONIZED來實作同步功能。

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才能成功。

可以看一下以下的圖,便于了解用:

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

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了。

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

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 methodinfo structure by the ACCSYNCHRONIZED 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解析——如果你願意一層一層剝開我的心

Synchronized第二層的總結

  • 同步代碼塊是通過monitorenter和monitorexit來實作,當線程執行到monitorenter的時候要先獲得monitor鎖,才能執行後面的方法。當線程執行到monitorexit的時候則要釋放鎖。
  • 同步方法是通過中設定ACCSYNCHRONIZED标志來實作,當線程執行有ACCSYNCHRONI标志的方法,需要獲得monitor鎖。
  • 每個對象維護一個加鎖計數器,為0表示可以被其他線程獲得鎖,不為0時,隻有目前鎖的線程才能再次獲得鎖。
  • 同步方法和同步代碼塊底層都是通過monitor來實作同步的。
  • 每個對象都與一個monitor相關聯,線程可以占有或者釋放monitor。

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

monitor螢幕

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

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

作業系統的管程

  • 管程 (英語: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中幾個關鍵字段的含義如圖所示:

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

工作機理

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

Synchronized解析——如果你願意一層一層剝開我的心
  • 想要擷取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關聯的呢?直接先看圖:

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

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

對象的記憶體布局

在HotSpot虛拟機中,對象在記憶體中存儲的布局可以分為3塊區域:對象頭(Header),執行個體資料(Instance Data)和對象填充(Padding)。

Synchronized解析——如果你願意一層一層剝開我的心
  • 執行個體資料:對象真正存儲的有效資訊,存放類的屬性資料資訊,包括父類的屬性資訊;
  • 對齊填充:由于虛拟機要求 對象起始位址必須是8位元組的整數倍。填充資料不是必須存在的,僅僅是為了位元組對齊。
  • 對象頭:Hotspot虛拟機的對象頭主要包括兩部分資料:Mark Word(标記字段)、Class Pointer(類型指針)。

對象頭

對象頭主要包括兩部分資料:Mark Word(标記字段)、Class Pointer(類型指針)。

Synchronized解析——如果你願意一層一層剝開我的心
  • Class Pointer:是對象指向它的類中繼資料的指針,虛拟機通過這個指針來确定這個對象是哪個類的執行個體
  • Mark Word : 用于存儲對象自身的運作時資料,它是實作輕量級鎖和偏向鎖的關鍵。

Mark word

Mark Word 用于存儲對象自身的運作時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀态标志、線程持有的鎖、偏向線程 ID、偏向時間戳等。

在32位的HotSpot虛拟機中,如果對象處于未被鎖定的狀态下,那麼Mark Word的32bit空間裡的25位用于存儲對象哈希碼,4bit用于存儲對象分代年齡,2bit用于存儲鎖标志位,1bit固定為0,表示非偏向鎖。其他狀态如下圖所示:

Synchronized解析——如果你願意一層一層剝開我的心
  • 前面分析可知,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的實作原理
  • 《深入了解Java虛拟機》
  • 深入了解多線程(五)—— Java虛拟機的鎖優化技術