Java鎖

工作中經常遇到需要用鎖來控制并發的問題,java中提供一個鎖神器關鍵字-Synchronized。通過它可以來解決多線程問題。與Java中另一個Lock鎖相比,之前一直覺得Synchronized是重量級的鎖,很耗性能,真的是這樣嗎?

Synchronized的實現原理

synchronized可以保證方法或者代碼塊在運行時,同一時刻只有一個方法可以進入到臨界區,同時它還可以保證共享變量的內存可見性

同步的基礎

Java中每一個對象都可以作為鎖,這是synchronized實現同步的基礎:

  • 普通同步方法,鎖是當前實例對象
  • 靜態同步方法,鎖是當前類的class對象
  • 同步方法塊,鎖是括號里面的對象

當一個線程訪問同步代碼塊時,它首先是需要得到鎖才能執行同步代碼,當退出或者拋出異常時必須要釋放鎖,那么它是如何來實現這個機制的呢?我們先看一段簡單的代碼:

public class SynchronizedTest {

public synchronized void test1() {
    //do something
}

public void test2() {
    synchronized (this) {
        //do something 
    }
}
image.png

同步的原理

???JVM規范規定JVM基于進入和退出Monitor對象來實現方法同步和代碼塊同步,但兩者的實現細節不一樣。代碼塊同步是使用monitorenter和monitorexit指令實現,而方法同步是使用另外一種方式實現的,細節在JVM規范里并沒有詳細說明,而是在Class文件的方法表中將該方法的access_flags字段中的synchronized標志位置1,表示該方法是同步方法并使用調用該方法的對象或該方法所屬的Class在JVM的內部對象表示Class做為鎖對象。

???monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處, JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個 monitor 與之關聯,當且一個monitor 被持有后,它將處于鎖定狀態。線程執行到 monitorenter 指令時,將會嘗試獲取對象所對應的 monitor 的所有權,即嘗試獲得對象的鎖。

???在虛擬機規范的要求,在執行monitorenter指令時,首先嘗試獲取對象的鎖,如果這個對象沒有被鎖,或者當前線程已經擁有這個對象的鎖,把鎖的計數器加1,相應的,在執行monitorexit指令時會將計數器減1,當計數器為0是,鎖被釋放。如果獲取對象的鎖失敗,那當前線程就要阻塞等待,直到對象鎖被另外一個線程釋放為止。

???在虛擬機規范對monitorenter和monitorexit的行為描述中,有兩點需要特別注意的。首先,synchronized同步塊對同一個線程來說是可重入的,不會出現自己把自己死鎖的問題。其次,同步塊在已進入的線程執行完之前,會阻塞后面其他線程的進入

Java對象頭

鎖存在Java對象頭里。如果對象是數組類型,則虛擬機用3個Word(字寬)存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,一字寬等于四字節,即32bit。

長度 內容 說明
32/64bit Mark Word 存儲對象的hashCode或鎖信息等。
32/64bit Class Metadata Address 存儲到對象類型數據的指針
32/64bit Array length 數組的長度(如果當前對象是數組)

Java對象頭里的Mark Word里默認存儲對象的HashCode,分代年齡和鎖標記位。32位JVM的Mark Word的默認存儲結構如下:

25 bit 4bit 1bit是否是偏向鎖 2bit鎖標志位
無鎖狀態 對象的hashCode 對象分代年齡 0 01

在運行期間Mark Word里存儲的數據會隨著鎖標志位的變化而變化。Mark Word可能變化為存儲以下4種數據:

image.png

在64位虛擬機下,Mark Word是64bit大小的,其存儲結構如下:


image.png

Monitor

???什么是Monitor?我們可以把它理解為一個同步工具,也可以描述為一種同步機制,它通常被描述為一個對象。

???與一切皆對象一樣,所有的Java對象是天生的Monitor,每一個Java對象都有成為Monitor的潛質,因為在Java的設計中 ,每一個Java對象自打娘胎里出來就帶了一把看不見的鎖,它叫做內部鎖或者Monitor鎖。
Monitor 是線程私有的數據結構,每一個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每一個被鎖住的對象都會和一個monitor關聯(對象頭的MarkWord中的LockWord指向monitor的起始地址),同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一標識,表示該鎖被這個線程占用。其結構如下:


image.png
  • Owner:初始時為NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖后保存線程唯一標識,當鎖被釋放時又設置為NULL;
  • EntryQ:關聯一個系統互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的線程。
  • RcThis:表示blocked或waiting在該monitor record上的所有線程的個數。
  • Nest:用來實現重入鎖的計數。
  • HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。
  • Candidate:用來避免不必要的阻塞或等待線程喚醒,因為每一次只有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然后因為競爭鎖失敗又被阻塞)從而導致性能嚴重下降。Candidate只有兩種可能的值0表示沒有需要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖。

鎖性能優化

自旋鎖與自適應自旋

???如果物理機有超過一個以上的處理器,讓后面請求鎖的那個線程“稍等一下”,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。為了讓線程等待,我們只需讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖
自旋次數默認是10次,參數-XX:PreBlockSpin來更改
在JDK1.6中引入了自適應的自旋鎖。自適應意味著自旋的時間不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的

鎖消除

???虛擬機即時編譯器在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的主要判斷依據來源于逃逸分析的數據支撐,如果判斷在一段代碼中,堆上的所有數據都不會逃逸出去從而被其他線程鎖訪問到,那就可以把它們當做棧上數據對待,認為它們是線程私有的,無須加鎖

鎖粗化

???如果一系統的連續操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作是出現在循環體中,那即使沒有線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗,如果虛擬機探測到有這樣一串零碎的操作都對同一個對象加鎖,將會把加鎖同步的范圍擴展(粗化)到整個操作序列的外部

輕量級鎖

???輕量級鎖時JDK1.6之后加入的新型鎖機制,它名字中的“輕量級”是相對于使用操作系統互斥量來實現的傳統鎖而言的,因此傳統的鎖機制就稱為“重量級”鎖。首先需要強調一點的是,輕量級鎖并不是用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。

獲取輕量級鎖步驟:

1)在代碼進入同步塊的時候,檢查此同步對象有沒有被鎖定(鎖標志狀態為“01”),若沒有被鎖定,虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced前綴,即Displaced Mark Word),否則執行步驟3)

2)虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,如果這個動作更新成功,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標志位(Mark Word的最后2bit)將轉變為“00”,即表示此對象處于輕量級鎖定狀態,否則執行步驟3)

3)虛擬機首先檢查對象的Mark Word是否指向當前線程的棧幀,如果指向,說明當前線程已經擁有這個對象的鎖,那就可以直接進入同步塊繼續執行,否則說這個鎖對象已經被其他線程搶占了。如果有兩條以上的線程爭用同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖,鎖標志的狀態值變為“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態

解除輕量級鎖步驟:

輕量級鎖的釋放也是通過CAS操作來進行的,主要步驟如下:

1)取出在獲取輕量級鎖保存在Displaced Mark Word中的數據;

2)用CAS操作將取出的數據替換當前對象的Mark
Word中,如果成功,則說明釋放鎖成功,否則執行(3);

3)如果CAS操作替換失敗,說明有其他線程嘗試獲取該鎖,則需要在釋放鎖的同時需要喚醒被掛起的線程。

image.png

偏向鎖

???偏向鎖也是JDK1.6中引入的一項鎖優化,它的目的是消除數據在無競爭情況下的同步原語,進一步提高程序的運行性能。如果說輕量級鎖是在無競爭的情況下使用CAS操作去消除同步使用的互斥量,那偏向鎖就是在無競爭的情況下把整個同步都消除了,連CAS操作都不做了

???偏向鎖的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是這個鎖會偏向于第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要再進行同步。

獲取鎖

1)檢測Mark Word是否為可偏向狀態,即是否為偏向鎖1,鎖標識位為01;
2)若為可偏向狀態,則測試線程ID是否為當前線程ID,如果是,則執行步驟(5),否則執行步驟(3);
3)如果線程ID不為當前線程ID,則通過CAS操作競爭鎖,競爭成功,則將Mark Word的線程ID替換為當前線程ID,否則執行線程(4);
4)通過CAS競爭鎖失敗,證明當前存在多線程競爭情況,當到達全局安全點,獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續往下執行同步代碼塊;
5)執行同步代碼塊

釋放鎖
???偏向鎖的釋放采用了一種只有競爭才會釋放鎖的機制,線程是不會主動去釋放偏向鎖,需要等待其他線程來競爭。偏向鎖的撤銷需要等待全局安全點(這個時間點是上沒有正在執行的代碼)。其步驟如下:

1)暫停擁有偏向鎖的線程,判斷鎖對象石是否還處于被鎖定狀態;
2)撤銷偏向蘇,恢復到無鎖狀態(01)或者輕量級鎖的狀態;

image.png

重量級鎖

重量級鎖通過對象內部的監視器(monitor)實現,其中monitor的本質是依賴于底層操作系統的Mutex Lock實現,操作系統實現線程之間的切換需要從用戶態到內核態的切換,切換成本非常高。

鎖的優缺點對比

優點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執行非同步方法比僅存在納秒級的差距。 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。 適用于只有一個線程訪問同步塊場景。
輕量級鎖 競爭的線程不會阻塞,提高了程序的響應速度。 如果始終得不到鎖競爭的線程使用自旋會消耗CPU。 追求響應時間。同步塊執行速度非常快。
重量級鎖 線程競爭不使用自旋,不會消耗CPU。 線程阻塞,響應時間緩慢。 追求吞吐量。 同步塊執行速度較長。

參考資料

周志明:《深入理解Java虛擬機》

方騰飛:《Java并發編程的藝術》

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,001評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,786評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,986評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,204評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,964評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,354評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,410評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,554評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,106評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,918評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,093評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,648評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,342評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,755評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,009評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,839評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,107評論 2 375

推薦閱讀更多精彩內容