上篇文章中向大家介紹了Synchronized的用法及其實作的原理。現在我們應該知道,Synchronized是通過對象内部的一個叫做螢幕鎖(monitor)來實作的。但是螢幕鎖本質又是依賴于底層的作業系統的Mutex Lock來實作的。而作業系統實作線程之間的切換這就需要從使用者态轉換到核心态,這個成本非常高,狀态之間的轉換需要相對比較長的時間,這就是為什麼Synchronized效率低的原因。
Java并發程式設計系列:
Java 并發程式設計:核心理論
Java并發程式設計:Synchronized及其實作原理
Java并發程式設計:Synchronized底層優化(輕量級鎖、偏向鎖)
Java 并發程式設計:線程間的協作(wait/notify/sleep/yield/join)
Java 并發程式設計:volatile的使用及其原理
一、重量級鎖
上篇文章中向大家介紹了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
是否是偏向鎖
鎖标志位
輕量級鎖
指向棧中鎖記錄的指針
00
重量級鎖
指向互斥量(重量級鎖)的指針
10
GC标記
空
11
偏向鎖
線程ID
Epoch
對象分代年齡
1
無鎖
對象的hashCode
“輕量級”是相對于使用作業系統互斥量來實作的傳統鎖而言的。但是,首先需要強調一點的是,輕量級鎖并不是用來代替重量級鎖的,它的本意是在沒有多線程競争的前提下,減少傳統的重量級鎖使用産生的性能消耗。在解釋輕量級鎖的執行過程之前,先明白一點,輕量級鎖所适應的場景是線程交替執行同步塊的情況,如果存在同一時間通路同一鎖的情況,就會導緻輕量級鎖膨脹為重量級鎖。
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):鎖粗化的概念應該比較好了解,就是将多次連接配接在一起的加鎖、解鎖操作合并為一次,将多個連續的鎖擴充成一個範圍更大的鎖。舉個例子:
這裡每次調用stringBuffer.append方法都需要加鎖和解鎖,如果虛拟機檢測到有一系列連串的對同一個對象加鎖和解鎖操作,就會将其合并成一次範圍更大的加鎖和解鎖操作,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖。
3、鎖消除(Lock Elimination):鎖消除即删除不必要的加鎖操作。根據代碼逃逸技術,如果判斷到一段代碼中,堆上的資料不會逃逸出目前線程,那麼可以認為這段代碼是線程安全的,不必要加鎖。看下面這段程式:
雖然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
作者:liuxiaopeng
部落格位址:http://www.cnblogs.com/paddix/
聲明:轉載請在文章頁面明顯位置給出原文連接配接。