天天看點

Java多線程程式設計-(11)-從volatile和synchronized的底層實作原理看Java虛拟機對鎖優化所做的努力

一、背景

對于Java來說我們知道,Java代碼首先會編譯成Java位元組碼,位元組碼被類加載器加載到JVM裡,JVM執行位元組碼,最終需要轉化為彙編指令在CPU上進行執行。

Java中所使用的并發機制依賴于JVM的實作和CPU的指令。

下邊我們對常見的實作同步的兩個關鍵字volatile和synchronized進行底層原理的分析,分析之餘我們就會了解到JVM在對鎖的優化所做的事情,這樣的話我們以後在使用這兩個關鍵字的時候還可以遊刃有餘。

二、volatile實作原理

相關文章:

Java多線程程式設計-(2)-可重入鎖以及Synchronized的其他基本特性

在這一篇文章中,我們知道了volatile的兩個主要作用:一個是volatile可以禁止指令的重排序優化,另一個作用是提供多線程通路共享變量的記憶體可見性。 禁止指令重排序優化是JVM記憶體模型的知識點,這裡不做學習,着重說一下可見性。

Java支援多個線程同時通路一個對象或者對象的成員變量,由于每個線程可以擁有這個變量的拷貝(雖然對象以及成員變量配置設定的記憶體是在共享記憶體中的,但是每個執行的線程還是可以擁有一份拷貝,這樣做的目的是加速程式的執行,這是現代多核處理器的一個顯著特性),是以程式在執行過程中,一個線程看到的變量并不一定是最新的。

關鍵字volatile可以用來修飾字段(成員變量),就是告知程式任何對該變量的通路均需要從共享記憶體中擷取,而對它的改變必須同步重新整理回共享記憶體,它能保證所有線程對變量通路的可見性。

volatile是輕量級的synchronized,他的意思是:當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。如果volatile變量修飾符使用恰當的話,它比synchronized的使用和執行成本更低,因為它不會引起線程上下文的切換和排程。

有volatile變量修飾的共享變量進行寫操作的時候會引發了兩件事情:

(1)将目前處理器緩存行的資料寫回到系統記憶體。

(2)這個寫回記憶體的操作會使在其他CPU裡緩存了該記憶體位址的資料無效。

這是因為,Java支援多個線程同時通路一個對象或者對象的成員變量,每個線程可以擁有這個變量的拷貝(雖然對象以及成員變量配置設定的記憶體是在共享記憶體中的,但是每個執行的線程還是可以擁有一份拷貝,這樣做的目的是加速程式的執行,這是現代多核處理器的一個顯著特性),是以程式在執行過程中,一個線程看到的變量并不一定是最新的。

我們的處理器為了提高處理速度,處理器不直接和記憶體進行通信,而是先将系統記憶體的資料讀到内部緩存(L1,L2或其他)後再進行操作,但操作完不知道何時會寫到記憶體。

關鍵字volatile可以用來修飾字段,就是告知程式任何對該變量的通路均需要從共享記憶體擷取(讀取時将本地記憶體置為無效,從共享記憶體讀取),而對它的改變必須同步重新整理回共享記憶體。保證所有線程對變量通路的可見性。

具體的實作細節如下(不需深究其原理,把握住上述兩點即可):

如果對聲明了volatile的變量進行寫操作,JVM就會向處理器發送一條Lock字首的指令,将這個變量所在緩存行的資料寫回到系統記憶體。但是,就算寫回到記憶體,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題。是以,在多處理器下,為了保證各個處理器的緩存是一緻的,就會實作緩存一緻性協定,每個處理器通過嗅探在總線上傳播的資料來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的記憶體位址被修改,就會将目前處理器的緩存行設定成無效狀态,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器緩存裡。

Java多線程程式設計-(11)-從volatile和synchronized的底層實作原理看Java虛拟機對鎖優化所做的努力

當然,volatile的實作原理遠不止這些,作為入門的了解我們牢牢把握住這兩點就行:一是強制把修改的資料寫回記憶體,另一個是在多處理器情況下使多處理器緩存的資料失效。這兩點對于一般的面試已經足以稱下場面,如果還相對其有跟深入的了解,可以另行搜尋資源進行學習。

二、synchronized實作原理

關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確定多個線程在同一個時刻,隻能有一個線程處于方法或者同步塊中,它保證了線程對變量通路的可見性和排他性。

示例代碼:

public class Synchronized {

    public static void main(String[] args) {
        synchronized (Synchronized.class) {
        }
        method();
    }

    public static synchronized void method() {
    }
}
           

編譯運作,然後使用指令:

javap.exe -v Synchronized.class

,(javap在“Java\jdk1.8.0_131\bin”目錄下)檢視結果:

Java多線程程式設計-(11)-從volatile和synchronized的底層實作原理看Java虛拟機對鎖優化所做的努力

大緻可以看出,對于上述代碼中的同步塊 的實作是通過

monitorenter

monitorexit

指令,而同步方法是依靠方法修飾符上的

ACC_SYNCHRONIZED

來完成的。

上述的兩種方式,無論采用的是哪一種方式,其本質是對一個對象的螢幕(monitor) 進行擷取,而這個擷取過程是排他的,也就是說同一時刻隻有一個線程擷取到由

synchronized

所保護對象的螢幕。

synchronized允許使用任何的一個對象作為同步的内容,是以任意一個對象都應該擁有自己的螢幕(monitor),當這個對象由同步塊或者這個對象的同步方法調用時,執行方法的線程必須先擷取到該對象的螢幕才能進入同步塊或者同步方法,而沒有擷取到螢幕(執行該方法)的線程将會被阻塞在同步塊和同步方法的入口處,進入

BLOCKED

狀态。

下圖描述了對象、對象的螢幕、同步隊列和執行線程之間的關系:

Java多線程程式設計-(11)-從volatile和synchronized的底層實作原理看Java虛拟機對鎖優化所做的努力

從上圖中我們可以看到,任意線程對Object(Object由synchronized保護)的通路,首先要獲得Object的螢幕。如果擷取失敗,線程進入同步隊列,線程狀态變為

BLOCKED

。當通路Object的前驅(獲得了鎖的線程)釋放了鎖,則該釋放操作喚醒阻塞在同步隊列中的線程,使其重新嘗試對螢幕的擷取。

三、Java虛拟機對synchronized的優化

synchronized相對于volatile是重量了很多,是以在以前很讓人诟病,但是從JDK 1.6版本以後為了減少獲得鎖和釋放鎖帶來的性能消耗而引入了偏向鎖和輕量級鎖,以及鎖的存儲結構和更新過程。

從JDK對

synchronized

的優化,可以看出Java虛拟機對鎖優化所做出的努力,下邊我們就分别學習一下什麼是偏向鎖、輕量級鎖、重量級鎖、自旋鎖。

在了解這四種鎖之前,我們先看一下

synchronized

鎖的存放位置,synchronized用的鎖是存在Java對象頭裡的 ,如果對象是數組類型,則虛拟機用3個字寬(Word)存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭。在32位虛拟機中,1字寬等于4位元組,即32bit,如下圖:

Java多線程程式設計-(11)-從volatile和synchronized的底層實作原理看Java虛拟機對鎖優化所做的努力

Java對象頭裡的

Mark Word

裡預設存儲對象的HashCode、分代年齡和鎖标記位。32位JVM的

Mark Word

的預設存儲結構如下圖所示:

Java多線程程式設計-(11)-從volatile和synchronized的底層實作原理看Java虛拟機對鎖優化所做的努力

在Java SE 1.6中,鎖一共有4種狀态,級别從低到高依次是:無鎖狀态、偏向鎖狀态、輕量級鎖狀态和重量級鎖狀态,這幾個狀态會随着競争情況逐漸更新。鎖可以更新但不能降級,意味着偏向鎖更新成輕量級鎖後不能降級成偏向鎖。這種鎖更新卻不能降級的政策,目的是為了提高獲得鎖和釋放鎖的效率。

Java多線程程式設計-(11)-從volatile和synchronized的底層實作原理看Java虛拟機對鎖優化所做的努力

下邊分别研究一下這幾個狀态。

四、偏向鎖

1、偏向鎖核心思想

偏向鎖是一種針對加鎖操作的優化手段,他的核心思想是:如果一個線程獲得了鎖,那麼鎖就進入了偏向模式。當這個線程再次請求鎖時,無需再做任何同步操作,這樣就節省了大量有關鎖申請的操作,進而提高了程式的性能。

2、偏向鎖設計初衷

為什麼會出現這種設計的方式那?這是因為根據HotSpot的作者研究,他發現鎖不僅不存在多線程競争,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了的偏向鎖這個概念。

3、偏向鎖擷取鎖流程

偏向鎖擷取鎖流程如下:

(1)當一個線程通路同步塊并擷取鎖時,會在對象頭和棧幀中的鎖記錄裡存儲鎖偏向的線程ID,以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,隻需簡單地測試一下對象頭的Mark Word裡是否存儲着指向目前線程的偏向鎖;

(2)如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的辨別是否設定成1(表示目前是偏向鎖);

(3)如果沒有設定,則使用CAS競争鎖;

(4)如果設定了,則嘗試使用CAS将對象頭的偏向鎖指向目前線程。

具體流程圖如下:

Java多線程程式設計-(11)-從volatile和synchronized的底層實作原理看Java虛拟機對鎖優化所做的努力

3、偏向鎖更新為輕量鎖

對于隻有一個線程通路的同步資源場景,鎖的競争不是很激烈,這時候使用偏向鎖是一種很好的選擇,因為連續多次極有可能是同一個線程請求相同的鎖。

但是在鎖競争比較激烈的場景,最有可能的情況是每次不同的線程來請求相同的鎖,這樣的話偏向鎖就會失效,倒不如不開啟這種模式,幸運的是Java虛拟機提供了參數可以讓我們有選擇的設定是否開啟偏向鎖。

如果偏向鎖失敗,虛拟機并不會立即挂起線程,而是使用輕量級鎖進行操作。

五、輕量級鎖

如果偏向鎖失敗,虛拟機并不會立即挂起線程,而是使用輕量級鎖進行操作。輕量級鎖他隻是簡單的将對象頭部作為指針,指向持有鎖的線程堆棧的内部,來判斷一個線程是否持有對象鎖。如果線程獲得輕量級鎖成功,則可以順利進入臨界區。如果輕量級鎖加鎖失敗,則表示其他線程搶先奪到鎖,那麼目前線程的輕量級鎖就會膨脹為重量級鎖。

六、自旋鎖

輕量級鎖就會膨脹為重量級鎖後,虛拟機為了避免線程真實的在作業系統層面挂起,虛拟機還會在做最後的努力–自旋鎖。

由于目前線程暫時無法獲得鎖,但是什麼時候可以獲得鎖時一個未知數。也許在幾個CPU時鐘周期之後,就可以獲得鎖。如果是這樣的話,直接把線程挂起肯定是一種得不償失的選擇,是以系統會在進行一次努力:他會假設在不就的将來,限額和從那個可以得到這把鎖,是以虛拟機會讓目前線程做幾個空循環(這也就是自旋鎖的意義),若經過幾個空循環可以擷取到鎖則進入臨界區,如果還是擷取不到則系統會真正的挂起線程。

那麼為什麼鎖的更新無法逆向那?

這是因為,自旋鎖無法預知到底會空循環幾個時鐘周期,并且會很消耗CPU,為了避免這種無用的自旋操作,一旦鎖更新為重量鎖,就不會再恢複到輕量級鎖,這也是為什麼一旦更新無法降級的原因所在。

七、三種鎖的優缺點的對比

Java多線程程式設計-(11)-從volatile和synchronized的底層實作原理看Java虛拟機對鎖優化所做的努力

八、Java虛拟機對鎖優化所做的努力

從Java虛拟機在優化synchronized的時候引入了:偏向鎖、輕量級鎖、重量級鎖以及自旋鎖,都可以看出Java虛拟機通過各種方式,盡量減少擷取所和釋放鎖所帶來的性能消耗。

但這還不全是Java虛拟機鎖做的努力,另外還有:鎖消除 、 CAS等等,更重要的還有一個無鎖的概念,包括上文中提到的自旋鎖,這些内容會在後續的文章中繼續學習。

另外,關于本篇主題的講解,可能偏向了volatile的原理講解、Java虛拟機對synchronized的優化以及中間的幾種鎖,這幾種鎖的具體含義也是面試的常客,是以需要花時間靜下心來仔細研究一二。

參考文章:

1、http://blog.csdn.net/wolegequdidiao/article/details/45116141

2、http://ifeve.com/volatile/

3、http://www.jianshu.com/p/425b44267afe

4、部分截圖和内容參考自《Java并發程式設計的藝術》