【轉】Java並發編程:Synchronized底層最佳化(偏向鎖、輕量級鎖)

來源:互聯網
上載者:User

標籤:狀態   方法   png   本質   開啟   情況下   一段   複製   根據   

 一、重量級鎖

  上篇文章中向大家介紹了Synchronized的用法及其實現的原理。現在我們應該知道,Synchronized是通過對象內部的一個叫做監視器鎖(monitor)來實現的。但是監視器鎖本質又是依賴於底層的作業系統的Mutex Lock來實現的。而作業系統實現線程之間的切換這就需要從使用者態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是為什麼Synchronized效率低的原因。因此,這種依賴於作業系統Mutex Lock所實現的鎖我們稱之為“重量級鎖”。JDK中對Synchronized做的種種最佳化,其核心都是為了減少這種重量級鎖的使用。JDK1.6以後,為了減少獲得鎖和釋放鎖所帶來的效能消耗,提高效能,引入了“輕量級鎖”和“偏向鎖”。

二、輕量級鎖 

  鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖定擴大到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)。JDK 1.6中預設是開啟偏向鎖和輕量級鎖的,我們也可以通過-XX:-UseBiasedLocking來禁用偏向鎖。鎖的狀態儲存在對象的標頭檔中,以32位的JDK為例:

鎖狀態

25 bit

4bit

1bit

2bit

23bit

2bit

是否是偏向鎖

鎖標誌位

輕量級鎖

指向棧中鎖記錄的指標

00

重量級鎖

指向互斥量(重量級鎖)的指標

10

GC標記

11

偏向鎖

線程ID

Epoch

對象分代年齡

1

01

無鎖

對象的hashCode

對象分代年齡

0

01

  “輕量級”是相對於使用作業系統互斥量來實現的傳統鎖而言的。但是,首先需要強調一點的是,輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用產生的效能消耗。在解釋輕量級鎖的執行過程之前,先明白一點,輕量級鎖所適應的情境是線程交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖。

1、輕量級鎖的加鎖過程

  (1)在代碼進入同步塊的時候,如果同步對象鎖狀態為無鎖狀態(鎖標誌位為“01”狀態,是否為偏向鎖為“0”),虛擬機器首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word。這時候線程堆棧與對象頭的狀態2.1所示。

  (2)拷貝對象頭中的Mark Word複製到鎖記錄中。

  (3)拷貝成功後,虛擬機器將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指標,並將Lock record裡的owner指標指向object mark word。如果更新成功,則執行步驟(3),否則執行步驟(4)。

  (4)如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象Mark Word的鎖標誌位設定為“00”,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態2.2所示。

  (5)如果這個更新操作失敗了,虛擬機器首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹為重量級鎖,鎖標誌的狀態值變為“10”,Mark Word中儲存的就是指向重量級鎖(互斥量)的指標,後面等待鎖的線程也要進入阻塞狀態。 而當前線程便嘗試使用自旋來擷取鎖,自旋就是為了不讓線程阻塞,而採用迴圈去擷取鎖的過程。

 

                     圖2.1 輕量級鎖CAS操作之前堆棧與對象的狀態

   

                      圖2.2 輕量級鎖CAS操作之後堆棧與對象的狀態

2、輕量級鎖的解鎖過程:

  (1)通過CAS操作嘗試把線程中複製的Displaced Mark Word對象替換當前的Mark Word。

  (2)如果替換成功,整個同步過程就完成了。

  (3)如果替換失敗,說明有其他線程嘗試過擷取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時,喚醒被掛起的線程。

三、偏向鎖

  引入偏向鎖是為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的擷取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由於一旦出現多線程競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的效能損耗必須小於節省下來的CAS原子指令的效能消耗)。上面說過,輕量級鎖是為了線上程交替執行同步塊時提高效能,而偏向鎖則是在只有一個線程執行同步塊時進一步提高效能。

1、偏向鎖擷取過程:

  (1)訪問Mark Word中偏向鎖的標識是否設定成1,鎖標誌位是否為01——確認為可偏向狀態。

  (2)如果為可偏向狀態,則測試線程ID是否指向當前線程,如果是,進入步驟(5),否則進入步驟(3)。

  (3)如果線程ID並未指向當前線程,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中線程ID設定為當前線程ID,然後執行(5);如果競爭失敗,執行(4)。

  (4)如果CAS擷取偏向鎖失敗,則表示有競爭。當到達全域安全點(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖定擴大為輕量級鎖,然後被阻塞在安全點的線程繼續往下執行同步代碼。

  (5)執行同步代碼。

2、偏向鎖的釋放:

  偏向鎖的撤銷在上述第四步驟中有提到偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全域安全點(在這個時間點上沒有位元組碼正在執行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態,撤銷偏向鎖後恢複到未鎖定(標誌位為“01”)或輕量級鎖(標誌位為“00”)的狀態。

3、重量級鎖、輕量級鎖和偏向鎖之間轉換

 

                                        圖 2.3三者的轉換圖

  該圖主要是對上述內容的總結,如果對上述內容有較好的瞭解的話,該圖應該很容易看懂。

四、其他最佳化 

1、適應性自旋(Adaptive Spinning):從輕量級鎖擷取的流程中我們知道當線程在擷取輕量級鎖的過程中執行CAS操作失敗時,是要通過自旋來擷取重量級鎖的。問題在於,自旋是需要消耗CPU的,如果一直擷取不到鎖的話,那該線程就一直處在自旋狀態,白白浪費CPU資源。解決這個問題最簡單的辦法就是指定自旋的次數,例如讓其迴圈10次,如果還沒擷取到鎖就進入阻塞狀態。但是JDK採用了更聰明的方式——適應性自旋,簡單來說就是線程如果自旋成功了,則下次自旋的次數會更多,如果自旋失敗了,則自旋的次數就會減少。

2、鎖粗化(Lock Coarsening):鎖粗化的概念應該比較好理解,就是將多次串連在一起的加鎖、解鎖操作合并為一次,將多個連續的鎖擴充成一個範圍更大的鎖。舉個例子:

 1 package com.paddx.test.string; 2  3 public class StringBufferTest { 4     StringBuffer stringBuffer = new StringBuffer(); 5  6     public void append(){ 7         stringBuffer.append("a"); 8         stringBuffer.append("b"); 9         stringBuffer.append("c");10     }11 }

  這裡每次調用stringBuffer.append方法都需要加鎖和解鎖,如果虛擬機器檢測到有一系列連串的對同一個對象加鎖和解鎖操作,就會將其合并成一次範圍更大的加鎖和解鎖操作,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖。

3、鎖消除(Lock Elimination):鎖消除即刪除不必要的加鎖操作。根據代碼逃逸技術,如果判斷到一段代碼中,堆上的資料不會逃逸出當前線程,那麼可以認為這段代碼是安全執行緒的,不必要加鎖。看下面這段程式:

 1 package com.paddx.test.concurrent; 2  3 public class SynchronizedTest02 { 4  5     public static void main(String[] args) { 6         SynchronizedTest02 test02 = new SynchronizedTest02(); 7         //啟動預熱 8         for (int i = 0; i < 10000; i++) { 9             i++;10         }11         long start = System.currentTimeMillis();12         for (int i = 0; i < 100000000; i++) {13             test02.append("abc", "def");14         }15         System.out.println("Time=" + (System.currentTimeMillis() - start));16     }17 18     public void append(String str1, String str2) {19         StringBuffer sb = new StringBuffer();20         sb.append(str1).append(str2);21     }22 }

雖然StringBuffer的append是一個同步方法,但是這段程式中的StringBuffer屬於一個局部變數,並且不會從該方法中逃逸出去,所以其實這過程是安全執行緒的,可以將鎖消除。下面是我本地執行的結果:

  為了盡量減少其他因素的影響,這裡禁用了偏向鎖(-XX:-UseBiasedLocking)。通過上面程式,可以看出消除鎖以後效能還是有比較大提升的。

  註:可能JDK各個版本之間執行的結果不盡相同,我這裡採用的JDK版本為1.6。

五、總結 

  本文重點介紹了JDk中採用輕量級鎖和偏向鎖等對Synchronized的最佳化,但是這兩種鎖也不是完全沒缺點的,比如競爭比較激烈的時候,不但無法提升效率,反而會降低效率,因為多了一個鎖定擴大的過程,這個時候就需要通過-XX:-UseBiasedLocking來禁用偏向鎖。下面是這幾種鎖的對比:

優點

缺點

適用情境

偏向鎖

加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距。

如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。

適用於只有一個線程訪問同步塊情境。

輕量級鎖

競爭的線程不會阻塞,提高了程式的響應速度。

如果始終得不到鎖競爭的線程使用自旋會消耗CPU。

追求回應時間。

同步塊執行速度非常快。

重量級鎖

線程競爭不使用自旋,不會消耗CPU。

線程阻塞,回應時間緩慢。

追求輸送量。

同步塊執行速度較長。

 

 參考文獻:

http://www.iteye.com/topic/1018932

http://www.infoq.com/cn/articles/java-se-16-synchronized

http://frank1234.iteye.com/blog/2163142

https://www.artima.com/insidejvm/ed2/threadsynch3.html

http://www.tuicool.com/articles/2aeAZn

【轉】Java並發編程:Synchronized底層最佳化(偏向鎖、輕量級鎖)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.