Java鎖機制之自動檔 - synchronized


引子

說起Java中的併發,有一個永恆的話題就是鎖機制。而提及Java中的鎖,我們一般認爲有兩種形式

  1. 通過synchronized關鍵字的實現

  2. 通過Lock接口的實現

網上關於兩種方式的對比已經比較詳盡,從使用角度來看synchronized關鍵字方式屬於自動檔,只需一條指令加鎖釋放全搞定,而Lock接口實現的鎖則相當於手動擋,需要關注加鎖、鎖中斷和解鎖的一系列細節,搞不好就得熄火。特別是在JDK1.6對於synchronized關鍵字做了大量的優化後,已經做到大部分業務都夠用了,所以廢話不在多,今天的主題:自動檔synchronized發車! 

同步對象

通過synchronized關鍵字修飾的部分我們一般稱之爲同步塊,而同步塊的實現是對於同步塊指定一個唯一訪問的對象。在實現過程中我們會涉及兩類同步對象,四種同步代碼實現方式。

實例對象同步

實例對象同步是指同步塊的唯一訪問對象是一個實例對象,實例對象同步的時候會嘗試獲取實例對象的monitor,這種時候需要注意的是不同線程如果同步的是同一個類的不同實例,是起不到對象同步的作用的。代碼如下:

Runnable test1 = new SynchronizedTest();
Runnable test2 = new SynchronizedTest();
new Thread(test1).start();
new Thread(test2).start();

實例對象同步我們又分爲兩種形式

  1. 實例方法的synchronized關鍵字

  2. 實例方法中的synchronized代碼塊

類對象同步

類對象同步是指同步塊的唯一訪問對象是一個類對象,類對象同步的時候會嘗試獲取類對象的monitor。所以在類對象同步的時候我們可能面臨過度鎖的問題,即類對象中的同步塊被線程鎖定導致所有類實例都無法被其他線程訪問。

類對象同步我們有兩種代碼實現方式

  1. 靜態方法的synchronized關鍵字

  2. 靜態方法中的synchronized代碼塊

底層實現

我們知道synchronized關鍵字主要是通過JVM層面進行實現,而這時候來看一下JVM的字節碼就是一個很有(顯)必(逼)要(格)的事情了。

針對上面提到的四種代碼實現方式,我們簡單擼一段代碼:

image.png

然後使用javac先編譯成class文件再使用javap來查看字節碼。

image.png

通過字節碼的結果比對我們發現,通過synchronized關鍵字修飾的同步塊都在字節碼中以monitorentermonitorexit的指令形式體現了出來,而通過關鍵字修飾的方法都沒有體現。

難道是加在方法上的關鍵字不起作用?

我們可以反過來想一下,如果針對這兩種情況加monitorentermonitorexit指令我們是加在哪裏呢?方法的開頭和結尾,那麼直接給這個方法加一個標記每次進入這個方法的時候通過標記去獲取鎖離開的時候再通過標記去釋放不就行了嗎。所以我們在方法定義下面看到了一行flag,而其中有一個ACC_SYNCHRONIZED正是起到了這個同步標記的作用。 

總結起來,synchronized關鍵字的底層實現分成顯式的指令實現和隱式的標記實現。

顯示實現主要針對同步塊,通過將同步塊代碼包含在monitorentermonitorexit指令中實現代碼塊的同步訪問。關於monitorentermonitorexit的實現原理,摘抄官方說明如下:

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.

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.

 

隱式實現主要針對同步方法,字節碼層面通過ACC_SYNCHRONIZED標誌位實現。當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置。如果設置了,執行線程將先獲取monitor,獲取成功之後才能執行方法體,方法執行完後再釋放monitor。關於monitor的獲取釋放規則和指令級別的實現一致。 

性能優化

synchronized關鍵字剛剛出現的時候,往往會成爲我們性能調優的常客,甚至很多代碼規範中明確指出儘可能的避免使用synchronized關鍵字來實現代碼同步。

終於在JDK1.6的時候,官方爸爸出手了,優化點包含:

  • 引入適應性自旋鎖

  在傳統鎖實現中,如果線程獲取鎖失敗則進入阻塞,CPU進行狀態切換,而往往狀態切換的代價是很大的。爲了解決這個問題,自旋應運而生,簡單來說就是通過不停的嘗試直到獲取到鎖。當等待的任務執行時間較長時,無限制的自旋會浪費CPU時間,一般會給自旋加一個固定的次數限制。適應性自旋則更進一步,由前一次在同一個鎖上的自旋時間和鎖的擁有者的狀態共同決定自旋的次數,如果前一次自旋成功並且當前擁有者正常運行則允許當前自旋佔用較多的CPU時間來進行自旋,如果在當前鎖上的自旋極少成功則分配較少的自旋次數避免資源浪費。

  • 通過逃逸分析的鎖消除

  主要是指JIT對於不存在同步訪問的同步塊進行鎖消除操作,具體來說就是在字節碼轉機器碼階段忽略掉不必要的monitorentermonitorexit指令。

  • 鎖粗化

  JIT在進行動態編譯的階段,如果發現前後兩個同步塊對同一個對象進行加鎖,則將鎖粗化成一個,避免了反覆獲取釋放鎖的開銷。

  • 通過鎖分級引入偏向鎖和輕量鎖

鎖分級的理念是基於鎖的應用場景進行了細分,研究發現在實際應用中大部分的同步場景都出現在無競爭狀態,小部分出現在存在少量競爭的場景,還有小部分是存在大量競爭的場景。

針對無競爭場景,提出了偏向鎖,通過在對象頭中存儲偏向的線程ID,下次再進入的時候就可以無代價獲取到鎖。

針對低競爭場景,推出了輕量鎖,通過CAS操作來嘗試替換對象頭中的線程指向,如果多次自旋失敗表明跳出了低競爭場景則進行鎖膨脹,升級爲重量級鎖。

其他特性

可重入性

所謂的可重入性,在鎖機制的上下文中我們可以理解爲如果一個線程獲取了對象的鎖之後多次訪問對象的同步塊都不會發生阻塞。

通俗來講可以理解爲,我們獲取了一個大房子(對象)的鑰匙,那麼以後想進哪間房間(同步塊)就進哪間房間。

提到可重入性,比較迷惑的是JUC中的ReentrantLock,讓人覺得如果要實現鎖的可重入性必須使用這個類,事實上synchronized實現的鎖默認也是可重入的。

公平性

鎖的公平性是指獲取鎖的順序嚴格的按照線程加鎖請求到達的順序,即滿足先到先得原則。

Javasynchronized實現的鎖是非公平的,而Lock接口的實現中如ReentrantLock中的鎖也是默認非公平的。不同點在於synchronized無法實現公平鎖,而ReentrantLock可以通過傳入參數指定使用公平鎖或者非公平鎖:

public ReentrantLock(boolean fair) {
         sync = fair ? new FairSync() : new NonfairSync();
}

典型應用

這裏主要看一下synchronizedConcurrentHashMap中的應用。

JDK1.8中對於ConcurrentHashMap有一項很重要的變更是取消了Segment的使用,取而代之的是使用Node數組結合synchronized的方式對單條記錄進行加鎖來進一步提高數據結構的併發性。

image.png

由於Segment是基於ReentrantLock實現的,我們不妨發散一下將這次升級解讀成:

  1. synchronized的優化到1.8版本已經經過足夠的驗證可以出現在基礎數據結構中

  2. 在鎖的使用上官方推薦synchronized的方式,後續應該還有持續發力

總結

在這篇文章中,我們從使用方式和底層實現兩個方面出發全面認識了Java中的加鎖方式synchronized關鍵字。

特別是經過JDK1.6的脫胎換骨,現在的synchronized關鍵字已經成爲了更多併發實現的首選。

通過對鎖機制的一般特性如重入性和公平性的理解,我們不難發現不管是synchronized方式還是Lock方式,實現思想上都是一脈相承的。而之所以並存的原因大概也是爲了可以通過多種選擇的提供達到相互促進的目的吧。

 


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章