1. 程式人生 > >Java多執行緒程式設計-(13)-從volatile和synchronized的底層實現原理看Java虛擬機器對鎖優化所做的努力

Java多執行緒程式設計-(13)-從volatile和synchronized的底層實現原理看Java虛擬機器對鎖優化所做的努力

一、背景

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

Java中所使用的併發機制依賴於JVM的實現和CPU的指令。

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

二、volatile實現原理

相關文章:

在這一篇文章中,我們知道了volatile的兩個主要作用:一個是volatile可以禁止指令的重排序優化,另一個作用是提供多執行緒訪問共享變數的記憶體可見性。

 禁止指令重排序優化是JVM記憶體模型的知識點,這裡不做學習,著重說一下可見性。

Java支援多個執行緒同時訪問一個物件或者物件的成員變數,由於每個執行緒可以擁有這個變數的拷貝(雖然物件以及成員變數分配的記憶體是在共享記憶體中的,但是每個執行的執行緒還是可以擁有一份拷貝,這樣做的目的是加速程式的執行,這是現代多核處理器的一個顯著特性),所以程式在執行過程中,一個執行緒看到的變數並不一定是最新的。

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

volatile是輕量級的synchronized,他的意思是:當一個執行緒修改一個共享變數時,另外一個執行緒能讀到這個修改的值

。如果volatile變數修飾符使用恰當的話,它比synchronized的使用和執行成本更低,因為它不會引起執行緒上下文的切換和排程。

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

(1)將當前處理器快取行的資料寫回到系統記憶體。

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

這是因為,Java支援多個執行緒同時訪問一個物件或者物件的成員變數,每個執行緒可以擁有這個變數的拷貝(雖然物件以及成員變數分配的記憶體是在共享記憶體中的,但是每個執行的執行緒還是可以擁有一份拷貝,這樣做的目的是加速程式的執行,這是現代多核處理器的一個顯著特性),所以程式在執行過程中,一個執行緒看到的變數並不一定是最新的。

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

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

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

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

0?wx_fmt=png

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

二、synchronized實現原理

關鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個執行緒在同一個時刻,只能有一個執行緒處於方法或者同步塊中,它保證了執行緒對變數訪問的可見性和排他性

示例程式碼:

0?wx_fmt=png

編譯執行,然後使用命令:javap.exe -v Synchronized.class,(javap在“Java\jdk1.8.0_131\bin”目錄下)檢視結果:

0?wx_fmt=png

大致可以看出,對於上述程式碼中的同步塊 的實現是通過monitorentermonitorexit 指令,而同步方法是依靠方法修飾符上的ACC_SYNCHRONIZED 來完成的。

上述的兩種方式,無論採用的是哪一種方式,其本質是對一個物件的監視器(monitor) 進行獲取,而這個獲取過程是排他的,也就是說同一時刻只有一個執行緒獲取到由synchronized所保護物件的監視器。

synchronized允許使用任何的一個物件作為同步的內容,因此任意一個物件都應該擁有自己的監視器(monitor),當這個物件由同步塊或者這個物件的同步方法呼叫時,執行方法的執行緒必須先獲取到該物件的監視器才能進入同步塊或者同步方法,而沒有獲取到監視器(執行該方法)的執行緒將會被阻塞在同步塊和同步方法的入口處,進入BLOCKED狀態。

下圖描述了物件物件的監視器同步佇列執行執行緒之間的關係:

0?wx_fmt=png

從上圖中我們可以看到,任意執行緒對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,如下圖:

0?wx_fmt=png

Java物件頭裡的Mark Word裡預設儲存物件的HashCode、分代年齡和鎖標記位。32位JVM的Mark Word的預設儲存結構如下圖所示:

0?wx_fmt=png

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

0?wx_fmt=png

下邊分別研究一下這幾個狀態。

四、偏向鎖

1、偏向鎖核心思想

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

2、偏向鎖設計初衷

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

3、偏向鎖獲取鎖流程

偏向鎖獲取鎖流程如下:

(1)當一個執行緒訪問同步塊並獲取鎖時,會在物件頭和棧幀中的鎖記錄裡儲存鎖偏向的執行緒ID,以後該執行緒在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下物件頭的Mark Word裡是否儲存著指向當前執行緒的偏向鎖;

(2)如果測試成功,表示執行緒已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設定成1(表示當前是偏向鎖);

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

(4)如果設定了,則嘗試使用CAS將物件頭的偏向鎖指向當前執行緒。

具體流程圖如下:

0?wx_fmt=png

3、偏向鎖升級為輕量鎖

對於只有一個執行緒訪問的同步資源場景,鎖的競爭不是很激烈,這時候使用偏向鎖是一種很好的選擇,因為連續多次極有可能是同一個執行緒請求相同的鎖。

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

如果偏向鎖失敗,虛擬機器並不會立即掛起執行緒,而是使用輕量級鎖進行操作。

五、輕量級鎖

如果偏向鎖失敗,虛擬機器並不會立即掛起執行緒,而是使用輕量級鎖進行操作。輕量級鎖他只是簡單的將物件頭部作為指標,指向持有鎖的執行緒堆疊的內部,來判斷一個執行緒是否持有物件鎖。如果執行緒獲得輕量級鎖成功,則可以順利進入臨界區。如果輕量級鎖加鎖失敗,則表示其他執行緒搶先奪到鎖,那麼當前執行緒的輕量級鎖就會膨脹為重量級鎖。

六、自旋鎖

輕量級鎖就會膨脹為重量級鎖後,虛擬機器為了避免執行緒真實的在作業系統層面掛起,虛擬機器還會在做最後的努力–自旋鎖

由於當前執行緒暫時無法獲得鎖,但是什麼時候可以獲得鎖時一個未知數。也許在幾個CPU時鐘週期之後,就可以獲得鎖。如果是這樣的話,直接把執行緒掛起肯定是一種得不償失的選擇,因此係統會在進行一次努力:他會假設在不就的將來,限額和從那個可以得到這把鎖,因此虛擬機器會讓當前執行緒做幾個空迴圈(這也就是自旋鎖的意義),若經過幾個空迴圈可以獲取到鎖則進入臨界區,如果還是獲取不到則系統會真正的掛起執行緒。

那麼為什麼鎖的升級無法逆向那?

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

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

0?wx_fmt=png

八、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併發程式設計的藝術》

注:檢視原始碼請點選閱讀原文,PC端效果更佳!

640?wx_fmt=jpeg