從偏向鎖是如何升級到重量級鎖的

簡介

在 jdk1.6 以前咱們會說 synchronized 是個重量級鎖,在此以後 JVM 對其作了不少的優化,以後使用 synchronized 線程在獲取鎖的時候根據競爭的狀態能夠是偏向鎖、輕量級鎖和重量級鎖。java

而在關於鎖的技術中,又出現了一些好比鎖粗化、鎖消除、自旋鎖、自適應自旋鎖他們又是什麼,本文後續會一一說明。編程

注意的是咱們討論的都是 synchronized 同步,即隱式加鎖。使用 Lock 加鎖的話它是另外的實現方式。安全

什麼是重量級鎖

要想知道 JVM 爲何對其進行優化,咱們就要先來了解下重量級鎖究竟是什麼,爲何要對其進行優化,咱們來看一段代碼bash

public synchronized void f() {
    System.out.println("hello world");
}
複製代碼

javap 反編譯後多線程

public synchronized void f();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3 // String hello world
         5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 3: 0
        line 4: 8

複製代碼

當某個線程訪問這個方法的時候,首先會去檢查是否有 ACC_SYNCHRONIZED 有的話就須要先得到對應的監視器鎖才能執行。併發

當方法結束或者中間拋出未被處理的異常的時候,監視器鎖就會被釋放。性能

在 Hotspot 中這些操做是經過 ObjectMonitor 來實現的,經過它提供的功能就可能作到獲取鎖,釋放鎖,阻塞中等待鎖釋放再去競爭鎖,鎖等待被喚醒等功能,咱們來探討下它是如何作到的。優化

每一個對象都持有一個 Monitor, Monitor 是一種同步機制,經過它咱們就能夠實現線程之間的互斥訪問,首先來列舉下 ObjectMonitor 的幾個咱們須要討論的關鍵字段ui

  • _owner,ObjectMonitor 目前被哪一個線程持有
  • _entryList,阻塞隊列(阻塞競爭獲取鎖的一些線程)
  • _WaitSet,等待隊列中的線程須要等待被喚醒(能夠經過中斷,singal,超時返回等)
  • _cxq,線程獲取鎖失敗放入 _cxq 隊列中
  • _recursions,線程重入次數,synchronized 是個可重入鎖

從一個線程開始競爭鎖到方法結束釋放鎖後阻塞隊列線程競爭鎖的執行的流程如上圖,而後來分別分析一下,在獲取鎖和釋放鎖着兩種狀況。this

獲取鎖的時候

釋放鎖的時候

在 jdk1.6 以前,synchronized 就直接會去調用 ObjectMonitor 的 enter 方法獲取鎖(第一張圖)了,而後釋放鎖的時候回去調用 ObjectMonitor 的 exit 方法(第二張圖)這被稱之爲重量級鎖,能夠看出它涉及到的操做複雜性。

那麼思考一下

若是說同一時間自己就只有一個線程去訪問它,那麼就算它存在共享變量,因爲不會被多線程同時訪問也不存在線程安全問題,這個時候其實就不須要執行重量級加鎖的過程。只須要在出現競爭的時候在使用線程安全的操做就好了

從而就引出了偏向鎖輕量級鎖

自旋鎖

自旋鎖自 jdk1.6 開始就默認開啓。因爲重量級鎖的喚醒以及掛起對都須要從用戶態轉入內核態調用來完成,大量併發的時候會給系統帶來比較大的壓力,因此就出現了自旋鎖,來避免頻繁的掛起以及恢復操做。

自旋鎖的意思是線程 A 已經得到了鎖在執行,那麼線程 B 在獲取鎖的時候,不阻塞,不放棄 CPU 執行時間直接進行死循環(有限定次數)不斷的去爭搶鎖,若是線程 A 執行速度很是快的完成了,那麼線程 B 可以較快的就得到鎖對象執行,從而避免了掛起和恢復線程的開銷,也能進一步的提高響應時間。

自旋鎖默認的次數爲 10 次能夠經過 -XX:PreBlockSpin 來更改

自適應性自旋

跟自旋鎖相似,不一樣的是它的自旋時間和次數再也不固定了。好比在同一個鎖對象上,上次自旋成功的得到了鎖,那麼 JVM 就會認爲下一次也能成功得到鎖,進而容許自旋更長的時間去獲取鎖。若是在同一個鎖對象上,不多有自旋成功得到過鎖,那額 JVM 可能就會直接省略掉自旋的過程。

自旋鎖和自適應鎖相似,雖然自旋等待避免了線程切換的開銷,可是他們都不放棄 CPU 的執行時間,若是鎖被佔用的時間很長,那麼可能就會存在大量的自旋從而浪費 CPU 的資源,因此自旋鎖是不能用來替代阻塞的,它有它適用的場景

偏向鎖

鎖會偏向於第一個執行它的線程,若是該鎖後續沒有其餘線程訪問過,那咱們就不須要加鎖直接執行便可。

若是後續發現了有其它線程正在獲取該鎖,那麼會根據以前得到鎖的線程的狀態來決定要麼將鎖從新偏向新的線程,要麼撤銷偏向鎖升級爲輕量級鎖。

Mark Word 鎖標識以下

thread ID - 是不是偏向鎖 鎖標誌位
thread ID epoch 1 01(未被鎖定)

線程 A - thread ID 爲 100,去獲取鎖的時候,發現鎖標誌位爲 01 ,偏向鎖標誌位爲 1 (能夠偏向),而後 CAS 將線程 ID 記錄在對象頭的 Mark Word,成功後

thread ID - 是不是偏向鎖 鎖標誌位
100 epoch 1 01(未被鎖定)

之後先 A 再次執行該方法的時候,只須要簡單的判斷一下對象頭的 Mark Word 中 thread ID 是不是當前線程便可,若是是的話就直接運行

假如此時有另一個線程線程 B 嘗試獲取該鎖,線程 B - thread ID 爲 101,一樣的去檢查鎖標誌位和是否能夠偏向的狀態發現能夠後,而後 CAS 將 Mark Word 的 thread ID 指向本身,發現失敗了,由於 thread ID 已經指向了線程 A ,那麼此時就會去執行撤銷偏向鎖的操做了,會在一個全局安全點(沒有字節碼在執行)去暫停擁有偏向鎖的線程(線程 A),而後檢查線程 A 的狀態,那麼此時線程 A 就有 2 種狀況了。

第一種狀況,線程 A 已經已經終止,那麼將 Mark Word 的線程 ID 置位空後,CAS 將線程 ID 偏向線程 B 而後就又回到上述又是偏向鎖線程的運行狀態了

thread ID - 是不是偏向鎖 鎖標誌位
101 epoch 1 01(未被鎖定)

第二種狀況,線程 A 處於活動狀態,那麼就會將偏向鎖升級爲輕量級鎖,而後喚醒線程 A 執行完後續操做,線程 B 自旋獲取輕量級鎖。

thread ID 是不是偏向鎖 鎖標誌位
0 00(輕量級鎖定)

能夠發現偏向鎖適用於從始至終都只有一個線程在運行的狀況,省略掉了自旋獲取鎖,以及重量級鎖互斥的開銷,這種鎖的開銷最低,性能最好接近於無鎖狀態,可是若是線程之間存在競爭的話,就須要頻繁的去暫停擁有偏向鎖的線程而後檢查狀態,決定是否從新偏向仍是升級爲輕量級別鎖,性能就會大打折扣了,若是事先可以知道可能會存在競爭那麼能夠選擇關閉掉偏向鎖

有的小夥伴會說存在競爭不就應該立馬升級爲重量級別鎖了嗎,不必定,下面講了輕量級鎖就會明白了。

輕量級鎖

若是說線程之間不存在競爭或者偶爾出現競爭的狀況而且執行鎖裏面的代碼的速度很是快那麼就很適合輕量級鎖的場景了,若是說偏向鎖是徹底取消了同步而且也取消了 CAS 和自旋獲取鎖的流程,它是隻須要判斷 Mark Word 裏面的 thread ID 是否指向本身便可(其它時間點有少量的判斷能夠忽略),那麼輕量級鎖就是使用 CAS 和自旋鎖來獲取鎖從而下降使用操做系統互斥量來完成重量級鎖的性能消耗

輕量級鎖的實現以下

JVM 會在當前線程的棧幀中建立用於存儲鎖記錄的空間,而後將對象頭的 Mark Word 複製到鎖記錄中,官方稱爲 Displaced Mark Word 而後線程嘗試使用 CAS 將對象頭的 Mark Word 替換爲指向鎖記錄的指針

假設線程 B 替換成功,代表成功得到該鎖,而後繼續執行代碼,此時 Mark Word 以下

線程棧的指針 鎖狀態
stack pointer 1 -> 執行線程 B 00(輕量級鎖)

此時線程 C 來獲取該鎖,CAS 修改對象頭的時候失敗發現已經被線程 B 佔用,而後它就自旋獲取鎖,結果線程 B 這時正好執行完成,線程 C 自旋獲取成功

線程棧的指針 鎖狀態
stack pointer 2 -> 線程 C 00(輕量級鎖)

此時線程 D 又獲取該鎖,發現被線程 C 佔用,而後它自旋獲取鎖,自旋默認 10 次後發現仍是沒法得到對應的鎖(線程 C 尚未釋放),那麼線程 D 就將 Mark Word 修改成重量級鎖

線程棧的指針 鎖狀態
stack pointer 2 -> 線程 C 10(重量級鎖)

而後這時線程 C 執行完成了,將棧幀中的 Mark Word 替換回對象頭的 Mark Word 的時候,發現有其它線程競爭該鎖(被線程 D 修改了鎖狀態)而後它釋放鎖而且喚醒在等待的線程,後續的線程操做就所有都是重量級鎖了

線程棧的指針 鎖狀態
10(重量量級鎖)

須要注意的是鎖一旦升級就不會降級了

鎖消除

鎖消除主要是 JIT 編譯器的優化操做,首先對於熱點代碼 JIT 編譯器會將其編譯爲機器碼,後續執行的時候就不須要在對每一條 class 字節碼解釋爲機器碼而後再執行了從而提高效率,它會根據逃逸分析來對代碼作必定程度的優化好比鎖消除,棧上分配等等

public void f() {
    Object obj = new Object();
    synchronized(obj) {
         System.out.println(obj);
    }
}
複製代碼

JIT 編譯器發現 f() 中的對象只會被一個線程訪問,那麼就會取消同步

public void f() {
    Object obj = new Object();
    System.out.println(obj);
}
複製代碼

鎖粗化

若是在一段代碼中連續的對同一個對象反覆加鎖解鎖,實際上是相對耗費資源的,這種狀況下能夠適當放寬加鎖的範圍,減小性能消耗。

當 JIT 發現一系列連續的操做都對同一個對象反覆加鎖和解鎖,甚至加鎖操做出如今循環體中的時候,會將加鎖同步的範圍擴散到整個操做序列的外部。

for (int i = 0; i < 10000; i++) {
    synchronized(this) {
        do();
    }
}
複製代碼

粗化後的代碼

synchronized(this) {
    for (int i = 0; i < 10000; i++) {
        do();
    }
}
複製代碼

參考:

相關文章
相關標籤/搜索