在Java多線程并發(fā)編程中synchronized一直都是元老級角色,很多人都會稱呼它為重量級鎖。但是隨著Java SE1.6對synchronized進(jìn)行了各種優(yōu)化之后,有些情況下使用它就并不那么重了。
首先聊一下synchronized實(shí)現(xiàn)同步的基礎(chǔ):Java中每一個(gè)對象都可以作為鎖具體表現(xiàn)為以下3重形式:
- 對于普通同步方法,鎖是當(dāng)前實(shí)例對象
public synchronized void method(){} - 對于靜態(tài)同步方法,鎖是當(dāng)前類的Class對象
public synchronized static void method(){} - 對于同步方法塊,鎖是synchronized括號里配置的對象。
public Object condition = new Object(); public void method(){ synchronized (condition) {} }
當(dāng)一個(gè)線程試圖訪問同步代碼塊時(shí),它必須先獲得鎖,退出或者拋出異常時(shí)必須釋放鎖。那么鎖到底存在哪里呢?鎖里面會存儲什么信息呢?我們首先得從Java的對象頭說起。
synchronized用的鎖是存在Java對象頭里的。如果對象是數(shù)組類型,則虛擬機(jī)用3個(gè)字寬存儲對象頭,如果對象非數(shù)組類型,則用2字寬存儲對象頭。在32位虛擬機(jī)中,一個(gè)字寬等于4個(gè)字節(jié),即32bit(位),64位虛擬機(jī)則是64bit(位)。對象頭的內(nèi)容如下:
| 長度 | 內(nèi)容 | 說明 |
|---|---|---|
| 32/64bit | Mark Word | 存儲對象的hashCode或鎖信息等 |
| 32/64bit | Class Metadata Address | 存儲到對象類型數(shù)據(jù)的指針 |
| 32/64bit | Array length | 數(shù)組的長度(如果是數(shù)組) |
我們主要來看看Mark Word的格式:
| 鎖狀態(tài) | 29 bit 或 61 bit | 1 bit 是否是偏向鎖? | 2 bit 鎖標(biāo)志位 |
|---|---|---|---|
| 無鎖 | 0 | 01 | |
| 偏向鎖 | 線程ID | 1 | 01 |
| 輕量級鎖 | 指向棧中鎖記錄的指針 | 此時(shí)這一位不用于標(biāo)識偏向鎖 | 00 |
| 重量級鎖 | 指向互斥量(重量級鎖)的指針 | 此時(shí)這一位不用于標(biāo)識偏向鎖 | 10 |
| GC標(biāo)記 | 此時(shí)這一位不用于標(biāo)識偏向鎖 | 11 |
Java SE1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java SE1.6中,鎖一共有四種狀態(tài),級別從低到高依次是:
- 無鎖狀態(tài)
- 偏向鎖狀態(tài)
- 輕量級鎖狀態(tài)
- 重量級鎖狀態(tài)
這幾種狀態(tài)會隨著競爭情況逐漸升級,鎖可以升級但是不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。
并不是所有的鎖都不能降級,但是在synchronized鎖中是無法降級的。(ReentrantReadWriteLock寫鎖可降級為讀鎖)
偏向鎖
HotSpot的作者經(jīng)過研究發(fā)現(xiàn),大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖。
當(dāng)一個(gè)線程訪問同步塊并獲取鎖時(shí),會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進(jìn)入和退出同步塊時(shí)不需要進(jìn)行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲著指向當(dāng)前線程的偏向鎖。如果測試成功,表示線程已經(jīng)獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標(biāo)識是否設(shè)置成1(表示當(dāng)前是偏向鎖):如果沒有設(shè)置,則 使用CAS競爭鎖;如果設(shè)置了,則嘗試使用CAS將對象頭的偏向鎖指向當(dāng)前線程。
簡單理解就是對上表中鎖對象頭的是否是偏向鎖的位置設(shè)置個(gè)變量,通過該變量以及偏向鎖線程id等條件判斷結(jié)果是否為true,是則不需要加鎖/解鎖流程,否則進(jìn)入后面的資源競爭的流程。

撤銷偏向鎖
偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機(jī)制,所以當(dāng)其他線程嘗試競爭偏向鎖時(shí), 持有偏向鎖的線程才會釋放鎖。
偏向鎖的撤銷大概需要三個(gè)步驟:
- 等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒有正 在執(zhí)行的字節(jié)碼)暫停擁有偏向鎖的線程
- 遍歷偏向?qū)ο蟮逆i記錄,修復(fù)棧中的鎖記錄和對象頭的Mark Word(恢復(fù)到無鎖或者標(biāo)記對象不適合作為偏向鎖)
-
喚醒被停止的線程,將當(dāng)前鎖升級成輕量級鎖
偏向鎖撤銷升級為輕量級鎖操作流程
偏向鎖在Java 6和Java 7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動(dòng)幾秒鐘之后才激活,如有必要可以使用JVM參數(shù)來關(guān)閉延遲:
-XX:BiasedLockingStartupDelay=0。如果你確定應(yīng)用程序里所有的鎖通常情況下處于競爭狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:- UseBiasedLocking=false,那么程序默認(rèn)會進(jìn)入輕量級鎖狀態(tài)。
輕量級鎖
多個(gè)線程在不同時(shí)段獲取同一把鎖,即不存在鎖競爭的情況,也就沒有線程阻塞。針對這種情況,JVM采用輕量級鎖來避免線程的阻塞與喚醒。
輕量級鎖的加鎖
線程在執(zhí)行同步塊之前,JVM會先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當(dāng)前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當(dāng)前線程便嘗試使用自旋來獲取鎖。
輕量級鎖解鎖
輕量級解鎖時(shí),會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發(fā)生。如果失敗,表示當(dāng)前鎖存在競爭,鎖就會膨脹成重量級鎖。下圖是兩個(gè)線程同時(shí)爭奪鎖,導(dǎo)致鎖膨脹的流程圖。

因?yàn)樽孕龝腃PU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復(fù)到輕量級鎖狀態(tài)。當(dāng)鎖處于這個(gè)狀態(tài)下,其他線程試圖獲取鎖時(shí), 都會被阻塞住,當(dāng)持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進(jìn)行新一輪的奪鎖之爭。
重量級鎖
自旋是需要消耗CPU的,如果一直獲取不到鎖的話,那該線程就一直處在自旋狀態(tài),白白浪費(fèi)CPU資源。解決這個(gè)問題最簡單的辦法就是指定自旋的次數(shù),例如讓其循環(huán)10次,如果還沒獲取到鎖就進(jìn)入阻塞狀態(tài)。
但是JDK采用了更聰明的方式——適應(yīng)性自旋,簡單來說就是線程如果自旋成功了,則下次自旋的次數(shù)會更多,如果自旋失敗了,則自旋的次數(shù)就會減少。
自旋也不是一直進(jìn)行下去的,如果自旋到一定程度(和JVM、操作系統(tǒng)相關(guān)),依然沒有獲取到鎖,稱為自旋失敗,那么這個(gè)線程會阻塞。同時(shí)這個(gè)鎖就會升級成重量級鎖。
重量級鎖依賴于操作系統(tǒng)的互斥量(mutex) 實(shí)現(xiàn)的,而操作系統(tǒng)中線程間狀態(tài)的轉(zhuǎn)換需要相對比較長的時(shí)間,所以重量級鎖效率很低,但被阻塞的線程不會消耗CPU。
前面說到,每一個(gè)對象都可以當(dāng)做一個(gè)鎖,當(dāng)多個(gè)線程同時(shí)請求某個(gè)對象鎖時(shí),對象鎖會設(shè)置幾種狀態(tài)用來區(qū)分請求的線程:
- Contention List:所有請求鎖的線程將被首先放置到該競爭隊(duì)列
- Entry List:Contention List中那些有資格成為候選人的線程被移到Entry List
- Wait Set:那些調(diào)用wait方法被阻塞的線程被放置到Wait Set
- OnDeck:任何時(shí)刻最多只能有一個(gè)線程正在競爭鎖,該線程稱為OnDeck
- Owner:獲得鎖的線程稱為Owner
- !Owner:釋放鎖的線程
當(dāng)一個(gè)線程嘗試獲得鎖時(shí),如果該鎖已經(jīng)被占用,則會將該線程封裝成一個(gè)ObjectWaiter對象插入到Contention List的隊(duì)列的隊(duì)首,然后調(diào)用park函數(shù)掛起當(dāng)前線程。
當(dāng)線程釋放鎖時(shí),會從Contention List或EntryList中挑選一個(gè)線程喚醒,被選中的線程叫做Heir presumptive即假定繼承人,假定繼承人被喚醒后會嘗試獲得鎖,但synchronized是非公平的,所以假定繼承人不一定能獲得鎖。這是因?yàn)閷τ谥亓考夋i,線程先自旋嘗試獲得鎖,這樣做的目的是為了減少執(zhí)行操作系統(tǒng)同步操作帶來的開銷。如果自旋不成功再進(jìn)入等待隊(duì)列。這對那些已經(jīng)在等待隊(duì)列中的線程來說,稍微顯得不公平,還有一個(gè)不公平的地方是自旋線程可能會搶占了Ready線程的鎖。
如果線程獲得鎖后調(diào)用Object.wait方法,則會將線程加入到WaitSet中,當(dāng)被Object.notify喚醒后,會將線程從WaitSet移動(dòng)到Contention List或EntryList中去。需要注意的是,當(dāng)調(diào)用一個(gè)鎖對象的wait或notify方法時(shí),如當(dāng)前鎖的狀態(tài)是偏向鎖或輕量級鎖則會先膨脹成重量級鎖。
總結(jié)鎖的升級流程
- 檢查MarkWord里面是不是放的自己的ThreadId ,如果是,表示當(dāng)前線程是處于 “偏向鎖”
- 如果MarkWord不是自己的ThreadId,鎖升級,這時(shí)候,用CAS來執(zhí)行切換,新的線程根據(jù)MarkWord里面現(xiàn)有的ThreadId,通知之前線程暫停,之前線程將Markword的內(nèi)容置為空
- 在一個(gè)安全的時(shí)間點(diǎn)暫停擁有偏向鎖的線程,兩個(gè)線程都把鎖對象的HashCode復(fù)制到自己新建的用于存儲鎖的記錄空間,接著開始通過CAS操作, 把鎖對象的MarKword的內(nèi)容修改為自己新建的記錄空間的地址的方式競爭MarkWord
- 第三步中成功執(zhí)行CAS的獲得資源,失敗的則進(jìn)入自旋
- 自旋的線程在自旋過程中,成功獲得資源(即之前獲的資源的線程執(zhí)行完成并釋放了共享資源),則整個(gè)狀態(tài)依然處于輕量級鎖的狀態(tài)
- 如果自旋失敗,進(jìn)入重量級鎖的狀態(tài),這個(gè)時(shí)候,自旋的線程進(jìn)行阻塞,等待之前線程執(zhí)行完成并喚醒自己
各種鎖的優(yōu)缺點(diǎn)對比
| 鎖 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場景 |
|---|---|---|---|
| 偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法比僅存在納秒級的差距 | 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 適用于只有一個(gè)線程訪問同步塊場景 |
| 輕量級鎖 | 競爭的線程不會阻塞,提高了程序的響應(yīng)速度 | 如果始終得不到鎖競爭的線程使用自旋會消耗CPU | 追求響應(yīng)時(shí)間。同步塊執(zhí)行速度非???/td> |
| 重量級鎖 | 線程競爭不使用自旋,不會消耗CPU | 線程阻塞,響應(yīng)時(shí)間緩慢 | 追求吞吐量。同步塊執(zhí)行時(shí)間較長 |
以上部分內(nèi)容(表格,圖片,文字等)摘自RedSpider社區(qū)
