1 Java對象頭信息
Java對象在JVM中的結(jié)構(gòu)如下:
java對象包括:
Mark Word(存儲對象的hashCode或者鎖信息)
Class Pointer(存儲對象所屬Class對象的指針)
length (只有數(shù)組對象才有這個字段)
Instance Data/Array Data (存儲實際數(shù)據(jù))
Padding (對齊緩存,填充對象被8整除的最小byte)
對象都在32/64位機器中每個部分分別是32/64位,Class Pointer在64位機器默認開啟指針壓縮,只占用32位。
2 對象加鎖過程
對象加鎖使用的是Mark Word字段,如下是32位的Mark Word
通過synchronize
關(guān)鍵字給對象加鎖的過程如下:
無鎖:對象剛創(chuàng)建,未被加鎖;
偏向鎖:Mark Word存儲線程ID(默認開啟)
輕量級鎖(自旋鎖):Mark Word 存儲現(xiàn)在棧地址
重量級鎖:調(diào)用操作系統(tǒng)的metux(已經(jīng)不再JVM層面,發(fā)生了系統(tǒng)調(diào)用)
3 偏向鎖
JVM引入偏向鎖是為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執(zhí)行路徑,因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID
的時候依賴一次CAS原子指令(一旦出現(xiàn)多線程競爭的情況就必須撤銷偏向鎖)。
3.1偏向鎖獲取過程
訪問Mark Word中偏向鎖的標識是否設(shè)置成1,鎖標志位是否為01——確認為可偏向狀態(tài)。
如果為可偏向狀態(tài),則測試線程ID是否指向當前線程,如果是,進入步驟(5),否則進入步驟(3)。
如果線程ID并未指向當前線程,則通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中線程ID設(shè)置為當前線程ID,然后執(zhí)行(5);如果競爭失敗,執(zhí)行(4)。
如果CAS獲取偏向鎖失敗,則表示有競爭。當?shù)竭_全局安全點(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續(xù)往下執(zhí)行同步代碼。
執(zhí)行同步代碼。
3.2 偏向鎖的釋放
偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節(jié)碼正在執(zhí)行),它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀態(tài),撤銷偏向鎖后恢復到未鎖定(標志位為“01”)或輕量級鎖(標志位為“00”)的狀態(tài)。
4 輕量級鎖
輕量級鎖所適應的場景是線程交替執(zhí)行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖。
4.1 輕量級鎖的加鎖過程
代碼進入同步塊的時候,如果同步對象鎖狀態(tài)為無鎖狀態(tài),虛擬機將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間;
拷貝對象頭中的Mark Word復制到鎖記錄中。
虛擬機將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,并將Lock record里的owner指針指向object mark word。如果更新成功,則執(zhí)行步驟(4),否則執(zhí)行步驟(5);
如果這個更新動作成功了,那么這個線程就擁有了該對象的鎖,并且對象Mark Word的鎖標志位設(shè)置為“00”,即表示此對象處于輕量級鎖定狀態(tài);
如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經(jīng)擁有了這個對象的鎖,那就可以直接進入同步塊繼續(xù)執(zhí)行。否則說明多個線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖,自旋失敗后,輕量級鎖就要膨脹為重量級鎖,鎖標志的狀態(tài)值變?yōu)椤?0”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,后面等待鎖的線程也要進入阻塞狀態(tài)。
4.2 輕量級鎖的解鎖過程
- 通過CAS操作嘗試把線程中復制的 Mark Word 對象替換當前的 Mark Word。
- 如果替換成功,整個同步過程就完成了。
- 如果替換失敗,說明有其他線程嘗試過獲取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時,喚醒被掛起的線程。
5 重量級鎖
synchronize的實現(xiàn)過程:
java語言層級:synchronize關(guān)鍵字實現(xiàn)加鎖
字節(jié)碼層級:moniterenter,moniterexit字節(jié)碼實現(xiàn)加鎖
JVM層級:偏向鎖->輕量級鎖->重量級鎖(已經(jīng)不在JVM,調(diào)用的操作系統(tǒng)的互斥量)
操作系統(tǒng)層級:申請操作系統(tǒng)的metux互斥鎖實現(xiàn)加鎖,會阻塞線程
硬件層級:通過 lock comxchg(CMS)實現(xiàn)獲取鎖資源,Lock前綴指令保障一致性
注意:lock前綴指令的功能:Synchronize, volatile,CMS都是使用這個實現(xiàn)
- 鎖總線,其它CPU對內(nèi)存的讀寫請求都會被阻塞,直到鎖釋放,不過實際后來的處理器都采用鎖緩存替代鎖總線,因為鎖總線的開銷比較大,鎖總線期間其他CPU沒法訪問內(nèi)存(原子性保障)
- lock后的寫操作會回寫已修改的數(shù)據(jù),同時讓其它CPU相關(guān)緩存行失效,從而重新從主存中加載最新的數(shù)據(jù)(可見性保障)
- 不是內(nèi)存屏障卻能完成類似內(nèi)存屏障的功能,阻止屏障兩遍的指令重排序(有序性保障)
當鎖膨脹成重量級鎖的時候,在JVM中當前鎖對象關(guān)聯(lián)的ObjectMonitor對象。
ObjectMonitor對象的數(shù)據(jù)結(jié)構(gòu)如下:
WaitSet:存放被
wait()
方法waiting的線程EntryList:存放獲取不到鎖被blocking的線程
OwnerThread:指向當前獲取到鎖的線程
recursions:記錄鎖重入的次數(shù)
EntryList是一個后進先出的雙向鏈表,AQS(ReentrantLock)是一個先進先出的雙向鏈表。
ObjectMoniter的流程:
拿到鎖的線程修改ObjectMoniter的OwnerThread指向自己,recursions加一
未拿到鎖的線程通過操作系統(tǒng)的阻塞,添加到EntryList隊列中
當線程調(diào)用
wait()
方法時,線程進入Waitset集合,并釋放鎖當調(diào)用
notify()
方法時,從Waitset集合取出,放入EntryList隊尾
注意:
Synchronize只有一個WaitSet,AQS可以創(chuàng)建多個Condition隊列(功能和Waitset類似)。