Synchronized低效的原因
在Java SE 1.6發佈前,使用Synchronized
關鍵字實現同步功能是比較低效的,很多人稱其爲重量級鎖.究其原理,是因爲Synchronized是通過對象內部的一個叫做監視器鎖(monitor)來實現的,而監視器鎖本質又是依賴於底層的操作系統的Mutex Lock來實現的。操作系統實現線程之間的切換需要從用戶態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是爲什麼Synchronized效率低的原因。
Java SE 1.6爲Synchronized帶來的優化(偏向鎖和輕量級鎖)
Java SE 1.6中爲了減少獲得鎖和釋放鎖帶來的性能消耗,引入了偏向鎖和輕量級鎖.要想弄清楚這兩個鎖的原理首先要了解Java對象頭,因爲Synchronized
用的鎖就存在Java對象頭中.
Java對象頭
Java對象頭的主要內容
內容 | 說明 |
---|---|
Mark Work | 存儲對象的hashCode或鎖信息等 |
Class Metadata Address | 存儲到對象類型數據的地址(即指向該對象的類型數據的指針) |
Array Length | 數組的長度(如果當前對象是數組) |
32位的JVM的Mark Work的默認存儲結構
鎖狀態 | 25bit | 4bit | 1bit 是否爲偏向鎖 | 2bit 鎖標誌位 |
---|---|---|---|---|
無鎖狀態 | 對象的hashCode | 對象分代年齡 | 0 | 01 |
Mark Word存儲結構中的數據會隨着標誌位的變化而變化.
Mark Word可能的狀態變化
在Java SE 1.6中,鎖一共有4中狀態,級別從低到高依次爲:
- 無鎖狀態
- 偏向鎖
- 輕量級鎖
- 重量級鎖
值得注意的是鎖狀態只能升級不能降級,也就是說輕量級可以膨脹爲重量級鎖,但是這個過程不可以逆轉.
1.偏向鎖
爲何引入偏向鎖
大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一個線程多次獲得.
引入偏向鎖是爲了在無多線程競爭的情況下儘量減少不必要的輕量級鎖執行路徑,因爲輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由於一旦出現多線程競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗必須小於節省下來的CAS原子指令的性能消耗)
偏向鎖是在只有一個線程執行同步塊時進一步提高性能。
偏向鎖的獲取過程:
- 訪問對象頭的
Mark Word
中的鎖標誌位是否爲01(確認爲可偏向狀態) - 如果爲可偏向狀態,則測試對象頭中
Mark Word
的線程ID是否指向當前線程,如果是則進入步驟5,否則進入步驟3 - 如果對象頭中
Mark Word
的線程ID並未指向當前線程,則當前線程通過CAS操作競爭鎖.如果競爭成功,則將Mark Word
中線程ID設置爲當前ID,然後執行步驟5.如果競爭失敗,則執行步驟4. - 如果CAS競爭偏向鎖失敗,則表示有競爭.當到到全局安全點時獲得偏向鎖的線程被掛起,偏向鎖升級爲輕量級鎖,然後被阻塞在安全點的線程繼續往下執行同步代碼.
- 執行同步代碼
注意:第四步中到達安全點safepoint會導致stop the word,時間很短。
偏向鎖的撤銷
使用一種等到競爭出現才釋放鎖的機制,即當其他線程競爭偏向鎖時,持有偏向鎖的線程會運行到全局安全點後掛起,判斷鎖對象是否處於被鎖定狀態,撤銷偏向鎖後恢復到未鎖定(標誌位爲“01”)或輕量級鎖(標誌位爲“00”)的狀態。
2.輕量級鎖
爲何引入輕量級鎖
輕量級鎖是爲了在沒有多線程競爭的前提下,減少傳統的重量級鎖使用產生的性能消耗。輕量級鎖所適應的場景是線程交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹爲重量級鎖。
輕量級鎖的加鎖過程:
- 在線程進入同步代碼塊時,如果同步對象鎖狀態爲無鎖狀態(鎖標誌位爲01,可否可偏向鎖爲0),JVM會首先在當前線程的棧幀中建立一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的
Mark Word
的拷貝,官方名稱爲’Displaced Mark Word’.這個時候線程堆棧與對象頭的狀態如圖1所示. - 拷貝對象頭的
Mark Word
複製到當前線程棧幀的鎖記錄中. - 拷貝成功後,虛擬機將使用CAS操作嘗試將對象的
Mark Word
更新爲指向鎖記錄(Lock Record)的指針,並將鎖記錄(Lock Record)裏的owner指針指向所對象的Mark Word
.如果CAS更新操作成功,則執行步驟4,否則執行步驟5. - 如果這個更新動作成功了,那麼這個線程就擁有了該對象的鎖,並且對象
Mark Word
的鎖標誌位設置爲“00”,即表示此對象處於輕量級鎖定狀態,這時候線程堆棧與對象頭的狀態如圖2所示。 - 如果這個更新操作失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。 而當前線程便嘗試使用自旋來獲取鎖.
輕量級鎖的撤銷
- 通過CAS操作嘗試把線程中複製的Displaced Mark Word對象替換當前的Mark Word。
- 如果替換成功,整個同步過程就完成了。
- 如果替換失敗,說明有其他線程嘗試過獲取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時,喚醒被掛起的線程。
3.鎖的優缺點對比
鎖 | 優點 | 缺點 | 適用場景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外的消耗,和執行非同步方法比時間相差無幾 | 如果線程存在鎖競爭,會帶來額外的鎖撤銷的消耗 | 適用於只有一個線程訪問同步塊場景 |
輕量級鎖 | 競爭的線程不會阻塞,提高了程序響應速度 | 如果始終得不到鎖競爭的線程,使用自旋會消耗CPU | 追求響應速度,同步塊執行速度非常快 |
重量級鎖 | 線程競爭不會自旋,不會消耗CPU | 線程阻塞,響應速度慢 | 追求吞吐量,同步塊執行速度慢或者執行時間長. |
Java 併發編程(一)Volatile原理剖析及使用
Java 併發編程(二)Synchronized原理剖析及使用
Java 併發編程(三)Synchronized底層優化(偏向鎖與輕量級鎖)
Java 併發編程(四)JVM中鎖的優化
Java 併發編程(五)原子操作類