我們知道 Synchronized
是 Java
中解決并發(fā)問(wèn)題的一種最常用的方法, 也是最簡(jiǎn)單的一種方法. 被也被稱(chēng)為內(nèi)置鎖.
Synchronized
的作用主要有三個(gè):
- 確保線程互斥的訪問(wèn)同步代碼
- 保證共享變量的修改能夠及時(shí)可見(jiàn)
- 有效解決重排序問(wèn)題憨琳。
?
從語(yǔ)法上講, Synchronized
總共有三種用法:
- 修飾普通方法, 鎖是當(dāng)前實(shí)例對(duì)象.
- 修飾靜態(tài)方法, 鎖是當(dāng)前類(lèi)的
class
對(duì)象. - 修飾代碼塊, 鎖是括號(hào)中的對(duì)象.
關(guān)于使用方式, 這里就不再進(jìn)行一一描述了. 我們直接進(jìn)入正題, 看 Synchronized
的底層實(shí)現(xiàn)原理是什么.
1. Synchronized 原理
首先, 我們先來(lái)看一段代碼, 使用了同步代碼塊和同步方法, 通過(guò)使用 javap
工具查看生成的 class
文件信息來(lái)分析 synchronized
關(guān)鍵字的實(shí)現(xiàn)細(xì)節(jié).
對(duì)代碼進(jìn)行反編譯后的結(jié)果如下
public static void main(java.lang.String[]) throws java.lang.Exception;
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic
3: dup
4: astore_1
5: monitorenter //---------------------------------------------1.
6: aload_1
7: monitorexit //---------------------------------------------2.
8: goto 16
11: astore_2
12: aload_1
13: monitorexit //---------------------------------------------3.
14: aload_2
15: athrow
16: return
...
public static synchronized void test();
descriptor: ()V
flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED //---------------------------------------------4.
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 21: 0
從生產(chǎn)的 class
信息中, 可以清楚的看到兩部分內(nèi)容
- 同步代碼塊中使用了
monitorenter
與monitorexit
指令. - 同步方法中依靠方法修飾符
flags
上的ACC_SYNCHRONIZED
實(shí)現(xiàn).
先看反編譯出 main
方法中標(biāo)記的 1 與 2. monitorenter / monitorexit
關(guān)于這兩條指令的作用, 參考 JVM
中對(duì)他們的描述如下:
monitorenter
每個(gè)對(duì)象有一個(gè)監(jiān)視器鎖 monitor
, 當(dāng) monitor
被占用時(shí)就會(huì)處于鎖定狀態(tài), 線程執(zhí)行 monitorenter
指令時(shí)嘗試獲取 monitor
的所有權(quán), 過(guò)程如下
- 如果
monitor
的進(jìn)入數(shù)為 0 , 則該線程進(jìn)入monitor
, 然后將進(jìn)入數(shù)設(shè)置為 1, 該線程即為monitor
的擁有者. - 如果線程已經(jīng)占有該
monitor
, 只是重新進(jìn)入, 則進(jìn)入monitor
的進(jìn)入數(shù)加 1. - 如果其他線程已經(jīng)占用了
monitor
, 則該線程進(jìn)入阻塞狀態(tài), 直到monitor
的進(jìn)入數(shù)為 0, 再?lài)L試獲取monitor
的所有權(quán).
?
monitorexit
執(zhí)行 monitorexit
的線程必須是對(duì)應(yīng) monitor
的所有者. 執(zhí)行指令時(shí), monitor
的進(jìn)入數(shù)減 1. 如果減 1 后進(jìn)入數(shù)為 0, 則線程退出 monitor
. 不再是這個(gè) monitor
的所有者. 其他被這個(gè) monitor
阻塞的線程可以嘗試去獲取這個(gè) monitor
的所有權(quán).
?
monitorenter
指令是在編譯后插入到同步代碼塊開(kāi)始的位置, 而monitorexit
是插入到方法的結(jié)束處和異常處. 這也就是為什么在 3 處會(huì)單獨(dú)有一個(gè)monitorexit
了.
?
ACC_SYNCHRONIZED
當(dāng)方法調(diào)用時(shí), 調(diào)用指令將檢查方法的 ACC_SYNCHRONIZED
訪問(wèn)標(biāo)志是否被設(shè)置, 如果設(shè)置了, 執(zhí)行線程將先獲取 monitor
, 獲取成功之后才能執(zhí)行方法體. 方法執(zhí)行完后再釋放 monitor
, 在方法執(zhí)行期間, 其他任何線程都無(wú)法再獲得同一個(gè) monitor
對(duì)象.
其實(shí)這個(gè)和上面 monitorenter
與 monitorexit
本質(zhì)上沒(méi)有區(qū)別, 只是方法的同步是一種隱式的方式來(lái)實(shí)現(xiàn)的, 無(wú)需通過(guò)字節(jié)碼來(lái)完成.
看完這些, 是不是覺(jué)得有點(diǎn)和 AQS 中的 state
相似? 如果看完了 從 LockSupport 到 AQS 的簡(jiǎn)單學(xué)習(xí) 這篇文章的朋友, 再來(lái)看這里, 我相信應(yīng)該會(huì)很容易理解.
這里既然說(shuō)到了監(jiān)視器鎖 monitor
, 我們一起來(lái)看這到底是什么.
?
2. 監(jiān)視器鎖 monitor
監(jiān)視器鎖 monitor
本質(zhì)是依賴(lài)于底層操作系統(tǒng)的 Mutex Lock
(互斥鎖) 來(lái)實(shí)現(xiàn)的. 每個(gè)對(duì)象都對(duì)應(yīng)于一個(gè)可稱(chēng)為 "互斥鎖" 的標(biāo)記, 這個(gè)標(biāo)記用來(lái)保證在任一時(shí)刻, 只能有一個(gè)線程訪問(wèn)該對(duì)象.
以下是 Mutex
的工作方式
- 申請(qǐng)
Mutex
. - 如果成功, 則持有該
Mutex
. - 如果失敗, 則進(jìn)行自旋, 自旋的過(guò)程就是在等待
Mutex
, 不斷發(fā)起Mutex gets
, 直到獲取Mutex
或者達(dá)到自旋次數(shù)的限制為止. - 依據(jù)工作模式的不同選擇
yiled
還是sleep
. - 若達(dá)到
sleep
限制或者主動(dòng)喚醒,又或者完成yiled
, 則繼續(xù)重復(fù)上面 4 點(diǎn), 直到獲得Mutex
為止.
?
3. 為什么說(shuō) Synchronized 是重量級(jí)鎖?
Synchronized
是通過(guò)對(duì)象內(nèi)部的一個(gè)叫監(jiān)視器鎖 monitor
來(lái)實(shí)現(xiàn)的, 監(jiān)視器鎖本質(zhì)又是依賴(lài)于底層的操作系統(tǒng)的互斥鎖 Mutex Lock
來(lái)實(shí)現(xiàn)的.
而從 Mutex Lock
(互斥鎖) 的工作流程我們可以得知是自旋和阻塞, 既然是阻塞那么肯定有喚醒. 由于 Java
的線程是映射到操作系統(tǒng)的原生線程之上的, 所以說(shuō)如果要阻塞或者喚醒一條線程, 都需要操作系統(tǒng)來(lái)幫忙完成, 這就需要從用戶(hù)態(tài)轉(zhuǎn)到內(nèi)核態(tài). 這個(gè)成本非常高, 狀態(tài)之間的轉(zhuǎn)換需要相對(duì)較長(zhǎng)的時(shí)間, 因此狀態(tài)轉(zhuǎn)換需要消耗很多處理器時(shí)間. 這就是為什么 Synchronized
效率低的原因. 因此, 這種依賴(lài)于操作系統(tǒng)互斥鎖 Mutex Lock
所實(shí)現(xiàn)的鎖, 我們稱(chēng)之為 "重量級(jí)鎖".
But, 在 JDK1.6
中為了獲得鎖和釋放鎖帶來(lái)的性能消耗, 引入了 偏向鎖 和 輕量級(jí)鎖, 使得 Synchronized
與 ReentrantLock
的性能基本持平. ReentrantLock
只是提供了比 Synchronized
更豐富的功能, (比如嘗試獲取鎖,嘗試釋放鎖等) 而不一定有更優(yōu)的性能, 所以在 Synchronized
能實(shí)現(xiàn)需求的情況下, 盡量還是優(yōu)先考慮使用 Synchronized
來(lái)進(jìn)行同步.
鎖一共有 4 種狀態(tài): 級(jí)別從低到高依次是: 無(wú)鎖狀態(tài) -> 偏向鎖狀態(tài) -> 輕量級(jí)鎖狀態(tài) -> 重量級(jí)鎖狀態(tài)
鎖可以升級(jí), 但是不能降級(jí).
在 JDK1.6 中, 除了引入偏向鎖與輕量級(jí)鎖的概念, 還有鎖消除, 鎖粗化等等.
接下來(lái)我們了解鎖是如何優(yōu)化前, 需要先了解一個(gè)重要的概念, 那就是 java
對(duì)象頭.
?
4. java 對(duì)象頭
Synchronized
鎖是存在 java
對(duì)象頭中的, 那么什么是 java
對(duì)象頭呢?
在 Hotspot
虛擬機(jī)中, 對(duì)象在內(nèi)存的分布為三個(gè)部分, 頭像頭, 實(shí)例數(shù)據(jù), 對(duì)齊填充. 而對(duì)象頭主要包括
- Mark Word (標(biāo)記字段) : 用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù), 如哈希碼, GC分代年齡, 鎖狀態(tài)標(biāo)志, 線程持有的鎖, 偏向線程 ID, 時(shí)間戳等等.
- Klass Pointer (類(lèi)型指針) : 存儲(chǔ)對(duì)象的類(lèi)型指針与斤,該指針指向它的類(lèi)元數(shù)據(jù)
Hotspot 虛擬機(jī): JVM 虛擬機(jī), 總的來(lái)說(shuō)是一種標(biāo)準(zhǔn)規(guī)范, 虛擬機(jī)有很多實(shí)現(xiàn)版本. 主要作用就是運(yùn)行 java 的類(lèi)文件的. 而 Hotspot 虛擬機(jī)是虛擬機(jī)的一種實(shí)現(xiàn), 它是 SUN 公司開(kāi)發(fā)的, 是 sun jdk 和 open jdk 中自帶的虛擬機(jī), 同時(shí)也是目前使用范圍最廣的虛擬機(jī).
Hotspot 與 JVM 兩者的區(qū)別一個(gè)是實(shí)現(xiàn)方式, 一個(gè)是標(biāo)準(zhǔn).
額外知識(shí)點(diǎn) :
Java
對(duì)象頭一般占有 2 個(gè)機(jī)器碼(在 32 位虛擬機(jī)中, 1 個(gè)機(jī)器碼等于 4 字節(jié), 也就是 32 bit), 但是如果對(duì)象是數(shù)組類(lèi)型, 則需要 3 個(gè)機(jī)器碼, 因?yàn)?JVM
虛擬機(jī)可以通過(guò)Java
對(duì)象的元數(shù)據(jù)信息確定Java
對(duì)象的大小, 但是無(wú)法從數(shù)組的元數(shù)據(jù)來(lái)確認(rèn)數(shù)組的大小, 所以用一塊來(lái)記錄數(shù)組的長(zhǎng)度. 下圖是Mark Word
默認(rèn)的存儲(chǔ)結(jié)構(gòu) (32 位虛擬機(jī))
對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無(wú)關(guān)的額外存儲(chǔ)成本, 但是考慮到虛擬機(jī)的空間效率, Mark Work
被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu), 以便在極小的空間內(nèi)存儲(chǔ)更多的信息. 也就是說(shuō), Mark Word
會(huì)隨著程序的運(yùn)行發(fā)生變化, 變化狀態(tài)如下(32 位虛擬機(jī))
我們現(xiàn)在知道鎖的狀態(tài)及相關(guān)信息是存在了 java
對(duì)象頭中的 Mark Word
中. 接著來(lái)看下鎖是如何優(yōu)化的. 無(wú)鎖狀態(tài)就不再說(shuō)了, 我們從最低的偏向鎖開(kāi)始.
?
5. 鎖優(yōu)化 - 偏向鎖
什么是偏向鎖
偏向鎖, 顧名思義, 它會(huì)偏向于第一個(gè)訪問(wèn)鎖的線程. 如果在運(yùn)行過(guò)程中只有一個(gè)線程訪問(wèn)同步塊, 會(huì)在對(duì)象頭和棧幀中的鎖記錄里存儲(chǔ)當(dāng)前線程的 ID, 以后該線程在進(jìn)入和退出同步塊時(shí)不需要進(jìn)行 CAS 操作來(lái)加鎖和解鎖, 只需要簡(jiǎn)單的判斷一下對(duì)象頭的 Mark Word
里是否存儲(chǔ)著當(dāng)前線程的ID.
?
為什么要引入偏向鎖
經(jīng)過(guò)研究發(fā)現(xiàn), 在大多數(shù)情況下, 鎖不僅不存在多線程競(jìng)爭(zhēng), 而且總是由同一線程多次獲得, 為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖, 減少不必要的 CAS 操作, 從而提高性能.
引入偏向鎖是為了在無(wú)多線程競(jìng)爭(zhēng)的情況下盡量減少不必要的輕量級(jí)鎖執(zhí)行路徑, 因?yàn)檩p量級(jí)鎖的獲取和釋放依賴(lài)多次的 CAS 原子指令. 而偏向鎖只需要在置換線程 ID 的時(shí)候依賴(lài)一次 CAS 原子指令. 因?yàn)橐坏┏霈F(xiàn)多線程競(jìng)爭(zhēng)的情況就必須撤掉偏向鎖, 膨脹為輕量級(jí)鎖. 所以偏向鎖的撤銷(xiāo)操作的性能損耗必須小于節(jié)省下來(lái)的 CAS 原子指令的性能消耗.
?
偏向鎖的三種狀態(tài)
- 匿名偏向: 這是允許偏向鎖的初始狀態(tài), 其
Mark Word
中的Thread ID
為0, 第一個(gè)試圖獲取該對(duì)象鎖的線程會(huì)遇到這種狀態(tài), 可以通過(guò)CAS
操作修改Thread ID
來(lái)獲取這個(gè)對(duì)象的鎖. - 可重偏向: 這個(gè)狀態(tài)下
Epoch
是無(wú)效的, 下一個(gè)線程會(huì)遇到這種情況, 在批量重偏向操作中, 所有未被線程持有的對(duì)象都會(huì)被設(shè)置成這個(gè)狀態(tài). 然后在下個(gè)線程獲取的時(shí)候能夠重偏向.(批量重偏向這里不深入分析, 有興趣的可以執(zhí)行研究一下)
- 已偏向: 這個(gè)狀態(tài)最簡(jiǎn)單, 就是被線程持有著, 此時(shí)
Thread ID
為其偏向的線程.
如果
JVM
啟用偏向鎖, 那么一個(gè)新建未被任何線程獲取的對(duì)象頭Mark Word
中的Thread Id
為0, 是可以偏向但未偏向任何線程, 被稱(chēng)為匿名偏向狀態(tài). 而無(wú)鎖狀態(tài)是不可偏向也未偏向任何線程, 不可再變?yōu)槠蜴i. 記住焙蚓!無(wú)鎖狀態(tài)不能變成偏向鎖!
?
偏向鎖獲取過(guò)程
- 在一個(gè)線程進(jìn)入同步塊的時(shí)候, 檢測(cè)
Mark Word
中是否為可偏向狀態(tài), 即[是否是偏向鎖] = 1, [鎖標(biāo)志位] = 01
- 若為可偏向狀態(tài), 檢測(cè)
Mark Word
中記錄的線程 ID 是否是當(dāng)前線程 ID, 如果是執(zhí)行步驟 5, 不是執(zhí)行步驟 3. 若為不可偏向狀態(tài), 直接執(zhí)行輕量級(jí)鎖流程. - 如果線程ID并未指向當(dāng)前線程罕伯,則通過(guò) CAS 操作競(jìng)爭(zhēng)鎖, 競(jìng)爭(zhēng)成功則將
Mark Word
中線程 ID 設(shè)置為當(dāng)前線程 ID. 然后執(zhí)行步驟 5, 否則執(zhí)行步驟 4. - 通過(guò) CAS 獲取偏向鎖失敗, 則表示有競(jìng)爭(zhēng).
(CAS 獲取偏向鎖失敗說(shuō)明至少有過(guò)其他線程曾獲得過(guò)偏向鎖, 因?yàn)榫€程不會(huì)主動(dòng)釋放偏向鎖)
. 當(dāng)?shù)竭_(dá)全局安全點(diǎn) (safepoint) 時(shí), 會(huì)首先掛起擁有偏向鎖的線程, 然后檢查持有偏向鎖的線程是否還活著,(因?yàn)橛锌赡艹钟衅蜴i的線程已經(jīng)執(zhí)行完畢, 但是該線程不會(huì)主動(dòng)釋放偏向鎖)
- 如果還存活, 接著判斷是否還在同步代碼塊中執(zhí)行.
- 若還在同步代碼塊中執(zhí)行, 直接升級(jí)為輕量級(jí)鎖.
- 若未在同步代碼塊中執(zhí)行, 則看是否可重偏向,
- 不可重偏向: 直接撤銷(xiāo)偏向鎖, 變?yōu)闊o(wú)鎖狀態(tài)后, 升級(jí)為輕量級(jí)鎖.
- 可重偏向: 修改
Mark Word
為匿名偏向狀態(tài), 通過(guò) CAS 將新線程 ID給Mark Word
賦值.喚醒新線程, 執(zhí)行同步代碼塊.
- 如果不存活, 也需要判斷是否可重偏向.
- 不可重偏向: 直接撤銷(xiāo)偏向鎖, 變?yōu)闊o(wú)鎖狀態(tài)后, 升級(jí)為輕量級(jí)鎖.
- 可重偏向: 修改
Mark Word
為匿名偏向狀態(tài), 通過(guò) CAS 將新線程 ID給Mark Word
賦值.喚醒新線程, 執(zhí)行同步代碼塊.
- 如果還存活, 接著判斷是否還在同步代碼塊中執(zhí)行.
JVM 維護(hù)了一個(gè)集合存放所有存活的線程, 通過(guò)遍歷該集合判斷是否有線程的 ID 等于持有偏向鎖線程的 ID, 有的話表示存活.
?
至于是否還在同步塊中執(zhí)行: 這個(gè)就需要說(shuō)到鎖記錄 Lock Record
當(dāng)代碼進(jìn)入同步塊的時(shí)候, 如果此時(shí)同步對(duì)象未被鎖定 (即 [鎖標(biāo)志位] = 01)
, 虛擬機(jī)會(huì)在當(dāng)前線程的棧幀中新建一個(gè)空間, 來(lái)存放鎖記錄 Lock Record
, 鎖記錄用于存儲(chǔ)記錄目前對(duì)象頭 Mark Word
的拷貝 (官方稱(chēng)之為 Displaced Mark Word)
以及記錄鎖對(duì)象的指針 owner
.
棧幀: 這個(gè)概念涉及的內(nèi)容較多, 不便于展開(kāi)敘述. 從理解下文的角度上來(lái)講, 只需要知道, 每個(gè)線程都有自己獨(dú)立的內(nèi)存空間, 棧幀就是其中的一部分. 里面可以存儲(chǔ)僅屬于該線程的一些信息.
在偏向鎖時(shí)也有
Lock Record
存在, 只不過(guò)作用不大.Lock Record
主要用于輕量級(jí)鎖和重量級(jí)鎖.
?
鎖記錄可以做什么?
可以統(tǒng)計(jì)重入的次數(shù), 判斷當(dāng)先線程是否還在同步塊中執(zhí)行.以及在輕量級(jí)鎖中會(huì)大量用到.
統(tǒng)計(jì)重入次數(shù)
線程每次進(jìn)入同步塊(即執(zhí)行monitorenter)
都會(huì)新建一個(gè)鎖記錄, 并將新建鎖記錄中的Displaced Mark Word
設(shè)為null
. 用來(lái)作為統(tǒng)計(jì)重入的次數(shù).owner
指向當(dāng)前的鎖對(duì)象.每次解鎖
(即執(zhí)行monitorexit)
的時(shí)候都會(huì)從最低的一個(gè)相關(guān)的鎖記錄移除. 所以可以通過(guò)遍歷線程棧中的Lock Record來(lái)判斷線程是否還在同步塊中.
下圖是一個(gè)重入三次的 Lock Record
示意圖.
為什么 JVM 選擇在線程棧幀中添加 Displaced Mark Word
為 null
的 Lock Record
來(lái)表示重入計(jì)數(shù)而不是將重入次數(shù)直接放在對(duì)象頭的 Mark Word
中呢. 之前說(shuō)過(guò), Mark Word
的大小是有限制的, 已經(jīng)存不下該信息了.
那么為什么不只創(chuàng)建一個(gè)鎖記錄在其中記錄重入次數(shù)呢? 這點(diǎn)我也沒(méi)有想明白. 如果有知道答案的朋友, 請(qǐng)留言告知一下, 萬(wàn)分感謝 !!!
?
?
偏向鎖的撤銷(xiāo)過(guò)程
偏向鎖的撤銷(xiāo)在上面第 4 點(diǎn)有說(shuō)到, 偏向鎖使用了一種等到競(jìng)爭(zhēng)出現(xiàn)才釋放偏向鎖的機(jī)制: 偏向鎖只有遇到其他線程嘗試競(jìng)爭(zhēng)偏向鎖時(shí), 持有偏向鎖的線程才會(huì)釋放, 線程本身不會(huì)主動(dòng)去釋放偏向鎖. 偏向鎖的撤銷(xiāo)需要等待全局安全點(diǎn)(在這個(gè)時(shí)間點(diǎn)上沒(méi)有字節(jié)碼正在執(zhí)行)
, 它會(huì)首先暫停擁有偏向鎖的線程, 判斷鎖對(duì)象是否處于被鎖定狀態(tài), 撤銷(xiāo)偏向鎖后恢復(fù)到無(wú)鎖或輕量級(jí)鎖的狀態(tài). 我們發(fā)現(xiàn), 這個(gè)開(kāi)銷(xiāo)其實(shí)還是挺大的, 所以如果某些同步代碼塊大多數(shù)情況下都是有兩個(gè)及以上的線程競(jìng)爭(zhēng)的話, 那么偏向鎖就會(huì)值一種累贅, 對(duì)于這種情況, 建議一開(kāi)始就把偏向鎖關(guān)閉.
注意: 偏向鎖撤銷(xiāo)是指在獲取偏向鎖的過(guò)程中因不滿(mǎn)足條件導(dǎo)致要將鎖對(duì)象改為非偏向鎖狀態(tài), 而偏向鎖釋放是指退出同步塊時(shí)的過(guò)程.
?
關(guān)閉偏向鎖
偏向鎖在 JDK 6
和 JDK 7
中默認(rèn)啟動(dòng)的. 由于偏向鎖是為了在只有一個(gè)線程執(zhí)行同步塊的時(shí)候提高性能. 如果能確定應(yīng)用程序里所有的鎖通常情況下處于競(jìng)爭(zhēng)狀態(tài), 可以通過(guò) JVM
參數(shù)關(guān)閉偏向鎖. 那么程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖的狀態(tài).
- 開(kāi)啟偏向鎖:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
- 關(guān)閉偏向鎖:
-XX:-UseBiasedLocking
?
偏向鎖流程圖
?
6. 鎖優(yōu)化 - 輕量級(jí)鎖
輕量級(jí)鎖是由偏向鎖升級(jí)來(lái)的, 偏向鎖運(yùn)行在一個(gè)線程進(jìn)入同步塊的情況下, 當(dāng)有第二個(gè)線程進(jìn)入產(chǎn)生鎖競(jìng)爭(zhēng)的情況下, 就會(huì)自動(dòng)升級(jí)為輕量級(jí)鎖. 其他線程會(huì)通過(guò)自旋的形式嘗試獲取鎖, 線程不會(huì)阻塞, 從而提高性能.
輕量級(jí)鎖的獲取主要有兩種情況
- 當(dāng)關(guān)閉偏向鎖功能時(shí)
- 由于多個(gè)線程競(jìng)爭(zhēng)偏向鎖導(dǎo)致偏向鎖升級(jí)為輕量級(jí)鎖.
?
輕量級(jí)鎖獲取過(guò)程
拷貝對(duì)象頭中的
Mark Word
到當(dāng)前線程棧幀的鎖記錄中. 并且虛擬機(jī)通過(guò)使用 CAS 操作嘗試將對(duì)象頭的Mark Word
更新為指向鎖記錄的指針, 并將鎖記錄里的owner
指針指向鎖對(duì)象. 這個(gè)操作成功執(zhí)行步驟 2, 失敗執(zhí)行步驟 3.如果這個(gè)更新動(dòng)作成功了, 那么當(dāng)前線程就擁有了該對(duì)象的鎖. 并且對(duì)象頭的
Mark Word
的鎖標(biāo)志位改為 00, 即表示此對(duì)象處于輕量級(jí)鎖定狀態(tài), 這時(shí)候線程堆棧與對(duì)象頭的狀態(tài)如下圖所示.
- 如果這個(gè)更新操作失敗了, 虛擬機(jī)首先會(huì)檢查對(duì)象的
Mark Word
是否指向當(dāng)前線程的棧幀- 如果不是: 說(shuō)明這個(gè)鎖對(duì)象已經(jīng)被其他線程搶占了, 則通過(guò)自旋稍微等待一下, 有可能持有鎖的線程很快就會(huì)釋放鎖.
但是當(dāng)自旋超過(guò)一定次數(shù)(默認(rèn)允許自旋 10 次, 可以通過(guò)虛擬機(jī)參數(shù)更改)
, 或者一個(gè)線程在持有鎖, 一個(gè)在自旋, 又有第三個(gè)線程來(lái)競(jìng)爭(zhēng)的時(shí)候, 就會(huì)膨脹為重量級(jí)鎖. 除了持有鎖的線程外, 其他線程阻塞. 對(duì)象頭Mark Word
中指向鎖記錄的指針改為指向重量級(jí)鎖(互斥量)的指針, 同時(shí)將鎖標(biāo)志位改為 10. - 如果是: 說(shuō)明當(dāng)前線程已經(jīng)擁有了這個(gè)對(duì)象的鎖, 現(xiàn)在是重入狀態(tài). 可直接進(jìn)入同步塊繼續(xù)執(zhí)行. 同時(shí)會(huì)添加一條鎖記錄
Lock Record
, 其中Displaced Mark Word
為null
, 起到一個(gè)重入計(jì)數(shù)器的作用.
- 如果不是: 說(shuō)明這個(gè)鎖對(duì)象已經(jīng)被其他線程搶占了, 則通過(guò)自旋稍微等待一下, 有可能持有鎖的線程很快就會(huì)釋放鎖.
?
輕量級(jí)鎖解鎖過(guò)程
- 遍歷當(dāng)前線程棧幀, 找到所有
owner
指向當(dāng)前鎖對(duì)象的Lock Record
- 如果
Lock Record
的Displaced Mark Word
為null
說(shuō)明這是一次重入, 刪除此鎖記錄, 接著continue
. 這即為一次解鎖結(jié)束. - 如果
Displaced Mark Word
不為null
, 并且對(duì)象頭中的Mark Word
仍然指向當(dāng)前線程的鎖記錄, 那就通過(guò) CAS 操作把對(duì)象頭中的Mark Word
恢復(fù)成為Lock Record
中拷貝過(guò)去的Displaced Mark Word
值. - 如果替換成功, 則
continue
. 也即為一次解鎖結(jié)束. - 如果替換失敗. 說(shuō)明外面有一個(gè)線程到達(dá)了自旋的總次數(shù), 或者外面至少還有兩個(gè)線程來(lái)競(jìng)爭(zhēng)鎖, 導(dǎo)致鎖已經(jīng)膨脹為重量級(jí)鎖. 從而改變了對(duì)象頭中
Mark Word
的內(nèi)容. 那就要在釋放鎖的同時(shí), 喚醒被掛起的線程. 重新?tīng)?zhēng)奪鎖訪問(wèn)同步塊.
輕量級(jí)鎖能提升程序同步性能的依據(jù)是 "對(duì)于絕大部分鎖在整個(gè)同步周期內(nèi)都是不存在競(jìng)爭(zhēng)的" 這是一個(gè)經(jīng)驗(yàn)數(shù)據(jù). 如果沒(méi)有競(jìng)爭(zhēng), 輕量級(jí)鎖使用 CAS 操作避免了使用互斥量的開(kāi)銷(xiāo), 但是如果存在競(jìng)爭(zhēng), 除了互斥量的開(kāi)銷(xiāo)外, 還額外發(fā)生了 CAS 操作. 因此在有競(jìng)爭(zhēng)的情況下, 輕量級(jí)鎖會(huì)比傳統(tǒng)的重量級(jí)鎖更慢.
?
7. 鎖優(yōu)化 - 重量級(jí)鎖
重量級(jí)鎖就是我們常說(shuō)的傳統(tǒng)意義上的鎖, 其利用操作系統(tǒng)底層的同步機(jī)制去實(shí)現(xiàn) Java
中的線程同步.
下圖是整個(gè)偏向鎖到輕量級(jí)鎖再膨脹為重量級(jí)鎖的流程圖. 可能不是很清晰.
?
8. 鎖優(yōu)化 - 鎖消除
何為鎖消除?
鎖消除即刪除不必要的加鎖操作, 在介紹這個(gè)之前, 先說(shuō)說(shuō) 逃逸和逃逸分析.
逃逸是指在方法之內(nèi)創(chuàng)建的對(duì)象, 除了在方法體之內(nèi)被引用之外, 還在方法體之外被引用. 也就是說(shuō)在方法體之外引用方法內(nèi)的對(duì)象, 在方法執(zhí)行完畢后, 方法中創(chuàng)建的對(duì)象應(yīng)該被 GC 回收. 但是由于該對(duì)象被其他變量引用, 導(dǎo)致 GC 無(wú)法回收.
這個(gè)無(wú)法被回收的對(duì)象成為 "逃逸" 對(duì)象. Java
中的逃逸分析, 就是對(duì)這種對(duì)象的分析.
那么接著回到鎖消除, Java JIT Java 即時(shí)編譯
會(huì)通過(guò)逃逸分析的方式, 去分析加鎖的代碼是否被一個(gè)或者多個(gè)線程使用, 或者等待被使用. 如果分析證實(shí), 只有一個(gè)線程訪問(wèn), 在編譯這個(gè)代碼段的時(shí)候, 就不會(huì)生成 Synchronized
關(guān)鍵字, 僅僅生代碼對(duì)應(yīng)的機(jī)器碼.
換句話說(shuō), 即使我們開(kāi)發(fā)人員加上了 Synchronized
, 但是只要 JIT 發(fā)現(xiàn)這段代碼只會(huì)被一個(gè)線程訪問(wèn), 也會(huì)把Synchronized
去掉.
?
9. 鎖優(yōu)化 - 鎖粗化
如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖茬暇,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,那即使沒(méi)有出現(xiàn)線程競(jìng)爭(zhēng)叶组,頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗。
如果虛擬機(jī)檢測(cè)到有一串零碎的操作都是對(duì)同一對(duì)象的加鎖经伙,將會(huì)把加鎖同步的范圍擴(kuò)展(粗化)到整個(gè)操作序列的外部.
public static void main(String[] args) throws Exception {
synchronized (object) {
test();
}
// 中間可穿插其他代碼
synchronized (object) {
test1();
}
synchronized (object) {
test2();
}
}
上面代碼存在三塊代碼段, 分割成三個(gè)臨界區(qū), 在 JIT 編譯時(shí)會(huì)將其合并成一個(gè)臨界區(qū). 用一個(gè)鎖對(duì)其進(jìn)行訪問(wèn)控制. 減少了鎖的獲取和釋放的次數(shù). 編譯后的等效代碼如下
public static void main(String[] args) throws Exception {
synchronized (object) {
test();
test1();
test2();
}
}
鎖粗化默認(rèn)是開(kāi)啟的扶叉。如果要關(guān)閉這個(gè)特性可以在 Java 程序的啟動(dòng)命令行中添加虛擬機(jī)參數(shù)-XX:-EliminateLocks
.
?
10. 鎖優(yōu)化 - 自旋鎖與自適應(yīng)自旋鎖
自旋鎖的來(lái)由
自旋鎖我們都知道是為了讓該線程執(zhí)行一段無(wú)意義的自旋, 等待一段時(shí)間, 不會(huì)被立刻掛起, 看持有鎖的線程是否會(huì)很快釋放.
可是為什么要引入自旋鎖呢?
首先互斥同步對(duì)性能最大的影響就是上面我們說(shuō)過(guò)的阻塞的實(shí)現(xiàn), 因?yàn)樽枞蛦拘丫€程的操作都需要由用戶(hù)態(tài)轉(zhuǎn)到內(nèi)核態(tài)中完成, 這些操作給系統(tǒng)的并發(fā)性能帶來(lái)很大壓力.
其次虛擬機(jī)的開(kāi)發(fā)團(tuán)隊(duì)也注意到許多應(yīng)用上面, 共享數(shù)據(jù)的鎖定狀態(tài)只會(huì)持續(xù)很短一段時(shí)間, 為了這一段很短的時(shí)間頻繁的阻塞喚醒線程非常不值得. 于是, 就引入了自旋鎖.
自旋鎖的缺點(diǎn)
自旋鎖雖然可以避免線程切換帶來(lái)的開(kāi)銷(xiāo), 但是它卻占用了處理器的時(shí)間. 如果持有鎖的線程很快就釋放了鎖, 那么自旋的效率就非常好. 反之, 自旋的線程就會(huì)白白浪費(fèi)處理器的資源帶來(lái)性能上的浪費(fèi). 所以說(shuō)自旋的次數(shù)必須要有一個(gè)限度, 例如 10 次. 如果超過(guò)這個(gè)次數(shù)還未獲取到鎖, 則就阻塞.
了解了自旋鎖, 那自適應(yīng)的自旋鎖呢?
自適應(yīng)自旋鎖
在 JDK1.6 中引入了自適應(yīng)的自旋鎖, 自適應(yīng)就意味著自旋的次數(shù)不再是固定的, 它是由前一次在同一個(gè)鎖上的自旋時(shí)間及鎖的擁有者的狀態(tài)來(lái)決定的.
如果在同一個(gè)鎖的對(duì)象上, 剛剛自旋成功獲得過(guò)鎖, 并且持有鎖的線程正在運(yùn)行中, 那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也很有可能再次成功, 進(jìn)而它將允許自旋等待持續(xù)更長(zhǎng)時(shí)間.
如果對(duì)于某個(gè)說(shuō), 自旋很少成功過(guò), 那么在以后要獲取這個(gè)鎖時(shí)將可能省略掉自旋的過(guò)程, 以避免浪費(fèi)處理器資源.
簡(jiǎn)單來(lái)說(shuō), 就是線程如果自旋成功了, 則下次自旋的次數(shù)會(huì)更多, 如果自旋失敗了, 則自旋的次數(shù)減少.
?
OK, 到這里也差不多了, 關(guān)于鎖的優(yōu)化基本就分析完了, 最后來(lái)個(gè)總結(jié)吧.
11. 總結(jié)
偏向鎖 | 輕量級(jí)鎖 | 重量級(jí)鎖 | |
---|---|---|---|
本質(zhì) | 取消同步操作 | CAS 操作代替互斥同步 | 互斥同步 |
優(yōu)點(diǎn) | 不阻塞, 執(zhí)行效率高, 只有第一次獲取偏向鎖時(shí)需要 CAS 操作, 后面只需要對(duì)比線程 ID | 不會(huì)阻塞 | 不會(huì)空耗 CPU |
缺點(diǎn) | 適用場(chǎng)景太局限, 若產(chǎn)生競(jìng)爭(zhēng), 會(huì)有額外的偏向鎖撤銷(xiāo)的消耗 | 自旋會(huì)浪費(fèi) CPU 資源 | 阻塞喚醒, 用戶(hù)態(tài)切換到內(nèi)核態(tài). 重量級(jí)操作 |
-
synchronized
的特點(diǎn): 保證了內(nèi)存可見(jiàn)性, 操作的原子性. -
synchronized
影響性能的原因- 加鎖和解鎖需要額外操作
- 互斥同步對(duì)性能最大的影響就是阻塞的時(shí)間, 因?yàn)樽枞麊拘褧?huì)由用戶(hù)態(tài)轉(zhuǎn)到內(nèi)核態(tài)中完成. 代價(jià)太大.
偏向鎖, 輕量級(jí)鎖, 重量級(jí)鎖都是 java 虛擬機(jī)自己內(nèi)部實(shí)現(xiàn), 當(dāng)執(zhí)行到 synchronized
同步代碼塊的時(shí)候, java 虛擬機(jī)會(huì)根據(jù)啟用的鎖和當(dāng)前線程的爭(zhēng)用情況來(lái)決定如何執(zhí)行同步操作.
在所有的鎖都啟用的情況下線程進(jìn)入臨界區(qū)時(shí)會(huì)先獲得偏向鎖, 如果已經(jīng)存在偏向鎖了, 則會(huì)嘗試獲取輕量級(jí)鎖, 啟用自旋鎖, 如果自旋也沒(méi)獲取到鎖, 則使用重量級(jí)鎖, 沒(méi)有獲取到鎖的線程被阻塞掛起, 知道持有鎖的線程執(zhí)行完同步代碼塊后去喚醒它們.
如果線程爭(zhēng)用激烈, 那么應(yīng)該禁用偏向鎖.
不同的鎖有不同特點(diǎn), 每種鎖只有在其特定的場(chǎng)景下, 才會(huì)有出色的表現(xiàn), java中沒(méi)有哪種鎖能夠在所有情況下都能有出色的效率. 引入這么多鎖的原因就是為了應(yīng)對(duì)不同的情況.
網(wǎng)上也摘抄了不少博客上的內(nèi)容, 自己整理了一下, 變成自己能看懂的.
參考來(lái)源:
Java synchronized原理總結(jié)
synchronized 底層原理
synchronized原理和鎖優(yōu)化策略(偏向/輕量級(jí)/重量級(jí))