Java鎖系列——2、Synchronized 鎖實現原理

概述

上篇博客我簡單介紹了 Synchronized 鎖的三種使用方法及部分特性。作爲開發者,適當的瞭解原理可以加深我們對它的理解。因此,本篇博客我打算從底層介紹一下 Synchronized 鎖實現的原理。


Synchronized 鎖實現原理

本篇博客分以下四個模塊展開:

  1. 對象頭
  2. Monitor 對象
  3. 顯式同步和隱式同步
  4. Monitor 與 阻塞喚醒

1、 對象頭

在 java 代碼中,對象創建完成後,在堆區分以下三個模塊存儲:

  • 對象頭
  • 實例數據
  • 填充數據

其中實例數據主要保存類屬性及數據信息。JVM 規定 Java 對象的起始地址必須是8的倍數,因此填充數據不存儲任何數據,僅僅爲了字節對齊。

最後我們來看對象頭區域:一般對象頭由以下兩個模塊組成:

  • Mark Word
  • Class Metadata Address

Class Metadata Address 模塊包含“引用”指向 Class 類對象,通過該標識確定對象的類型。

重點是 Mark Word 模塊,該模塊記錄當前對象的hash值,gc 分代年齡以及鎖信息。這裏的鎖信息包括是否偏向鎖以及鎖標誌位。默認情況下 32 位虛擬機中未加鎖對象的 Mark Word 結構如下

對象hashCode 對象分代年齡 固定爲0(加鎖狀態時表示是否偏向鎖) 鎖標誌位
25 bit 4 bit 1bit 2 bit

根據不同的鎖標誌位,我們可以確定當前鎖的狀態:無鎖、輕量級鎖、偏向鎖、重量級鎖等等。關於不同鎖的特性後面我們出博客專門介紹,本篇我們主要來看重量級鎖。

當鎖標誌爲10時,表示當前鎖狀態爲重量級鎖。此時存在指針指向該對象對應的鎖,也就是 Monitor 對象。Synchronized 鎖就是基於進入和退出 Monitor 對象實現的。下面我們具體看 Monitor 對象的數據結構。


2、Monitor 對象

Monitor 對象又叫管程對象,是 java Synchronized 鎖實現的原理。任何 java 對象都有一個管程對象和它相連,其中管程對象可以隨着對象的創建而創建,也可以在線程獲取某個對象鎖資源時創建,其中線程獲取的鎖資源就是 Monitor 管程對象的持有權。當 Monitor 對象被某個線程獲取後,該對象會處於鎖死狀態,也就是說當其他線程再來獲取當前對象的鎖資源時阻塞等待

Monitor 是由 ObjectMonitor 實現的,該類是由 C++ 編寫的。下面我們看一下該類的核心屬性:

ObjectMonitor() {
	_count = 0;
	_owner = NULL;
	_WaitSet = NULL;
	_EntryList = NULL ;
}

在上述代碼中,_count 屬性表示當前鎖重入的次數,_owner 指向當前獲取鎖的線程。_WaitSet 和 _EntryList 都是隊列,前者保存調用 wait() 方法阻塞的線程對象,後者保存正在等待鎖的線程對象。

下面我舉一個簡單的例子:線程A 和 線程B 爭搶 對象C 的鎖

  1. 對象C 的管程對象初始化,所有屬性都爲 0 或 NULL
  2. 線程A 和 線程B 進入該管程對象的 _EntryList 隊列中
  3. 線程A 搶佔到鎖,從 _EntryList 隊列中出來,管程對象 _owner 指向該線程,count 值加一
  4. 線程A 調用 wait() 方法釋放鎖,進入管程對象的 _WaitSet 隊列,_owner 指向爲空,count 值自減一
  5. 線程B 搶佔到鎖,從 _EntryList 隊列中出來,管程對象 _owner 指向該線程,count 值加一
  6. 線程B 再次搶該鎖,count 值繼續加一
  7. 線程B 調用該對象的 notify() 方法,線程A 從 _WaitSet 隊列中釋放,進入 _EntryList 隊列搶鎖
  8. 線程B 執行完第一塊同步代碼,count 值減一
  9. 線程B 執行完第二塊同步代碼,count 值減一,此時_count 值爲0,_owner 指向爲空
  10. 線程A 搶佔到鎖,_owner 指向線程A 對象,count 值加一
  11. 線程A 執行完同步代碼塊,count 值減一,_owner 指向爲空,兩個線程都執行完畢

從這個簡單的過程,我們可以看出:

  • _owner 標識判斷 Monitor 對象是否被佔用 以及 保證線程同步
  • _count 標識確定佔有 Monitor 對象的線程是否執行完同步代碼塊需要釋放鎖
  • _waitSet 標識保證調用 wait() 方法的線程被阻塞且不搶佔鎖
  • _EntrySet 標識保證後來的線程阻塞等待

以上這些也就是我理解的 Monitor 對象實現線程同步的原理。


3、顯式同步和隱式同步

上面介紹了 Monitor 對象關於同步的操作,下面我們來看 synchronized 具體是怎麼實現的。

根據 synchronized 使用方法的不同,它所實現的同步有以下兩種類型:顯式同步隱式同步

  1. 當 synchronized 修飾代碼塊時,通過指令 monitorenter 和 monitorexit 實現同步,這種方式也叫顯示同步

  2. 當 synchronized 修飾方法時,根據 ACC_SYNCHRONIZED 指令判斷是否同步,這種方式稱爲隱式同步


3-1、顯示同步

下面我們看一個使用 synchronized 修飾代碼塊的簡單代碼:

public class SynTest {
    int num = 0;

    public void add() {
        synchronized (this) {
            num++;
        }
    }
}

我們使用 javap 反編譯上述代碼會得到以下結果:

Compiled from "SynTest.java"
public class com.xxx.worker.lock.SynTest {
  int num;

  public com.xxx.worker.lock.SynTest();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: iconst_0
       6: putfield      #2                  // Field num:I
       9: return

  public void add();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: aload_0
       5: dup
       6: getfield      #2                  // Field num:I
       9: iconst_1
      10: iadd
      11: putfield      #2                  // Field num:I
      14: aload_1
      15: monitorexit
      16: goto          24
      19: astore_2
      20: aload_1
      21: monitorexit
      22: aload_2
      23: athrow
      24: return
    Exception table:
       from    to  target type
           4    16    19   any
          19    22    19   any
}

重點關注指令 monitorentermonitorexit。其中 monitorenter 指令指向同步代碼開始的模塊,monitorexit 指向同步代碼結束的模塊。通過這兩條指令標識被 synchronized 修飾的代碼塊

當執行 monitorenter 指令時:線程試圖獲取當前對象對應 Monitor 對象的持有權。

  1. 如果當前線程搶鎖成功,Monitor 對象 _owner 指向當前線程,_count 加一。
  2. 如果當前線程已經佔有鎖,Monitor 對象 _count 加一。
  3. 如果 Monitor 對象已經被其他線程持有,當前線程阻塞,進入 Monitor 對象的 _entrySet隊列。

當執行 monitorexit 指令時:當前線程將 Monitor 對象的 _count 值減一。

  1. 如果減完之後 _count 值爲0,說明當前線程已經執行完同步代碼塊,_owner 置爲空,釋放鎖資源
  2. 如果減完之後 _count 值不爲0,說明當前線程還沒有執行完同步代碼塊,不會釋放鎖資源

需要注意的一點是,每一個 monitorenter 指令都會對應一個 monitorexit 指令。即使線程執行過程中拋出異常,在捕獲異常後還會執行 monitorexit 指令。通過這種方式保證鎖對象最終一定會被釋放,確保多線程不會因爲某個線程佔有鎖異常終止而無法正常運轉。


3-2、隱式同步

下面我們看一個用 synchronized 修飾實例方法的簡單案例:

public class SynTest {
    int num = 0;
    
    public synchronized void add() {
        num++;
    }
}

我們使用 javap 反編譯上述代碼會得到以下結果:

Compiled from "SynTest.java"
...
public synchronized void add();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=3, locals=1, args_size=1
       0: aload_0
       1: dup
       2: getfield      #2                  // Field num:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field num:I
      10: return
    LineNumberTable:
      line 11: 0
      line 12: 10
}

從編譯後的字節碼來看,synchronized 修飾方法時,並沒有使用 monitorenter 指令,而是通過從運行時常量池讀取該方法的 ACC_SYNCHRONIZED 標識,通過該標識證明當前方法是一個同步方法。當線程執行該方法時,就會進行相應的同步操作。


4、Monitor 與 阻塞喚醒

最後我們再來聊一下 wait()、notify()、notifyAll() 這三個方法。

  • 之前我們提到這三個方法都需要在 synchronized 修飾的代碼塊中,否則拋出異常。

    其實原因是這樣的:wait()、notify(),notifyAll() 也是基於 Monitor 管程對象實現的。調用 wait() 方法讓當前線程放棄管程對象,進入 _WaitSet 隊列,而 notify() 和 notifyAll() 方法讓當前線程喚醒 _WaitSet 隊列中的線程。也就是說,無論這三個方法中哪一個,都需要擁有當前管程對象的持有權,而 synchronized 標識保證線程獲取管程對象的持有權,這也是爲什麼這三個方法都需要在 synchronized 修飾的代碼塊中。

  • 接下來我們再聊一下爲什麼這三個方法都是 Object 方法?

    其實是因爲任何 java 對象都會有一個鎖對象與之關聯,也就是說任何對象都可以調用 wait() 、notify()、notifyAll() 方法,而java語言所有對象繼承 Object 類,這也是這三個方法都是 Object() 方法的主要原因。

  • 最後我們來聊聊 join() 方法

    之前我提到過,join() 方法底層通過調用 wait() 方法實現。也就是說,讓調用 join() 方法的線程對象進入參數線程鎖對象的 _WaitSet 隊列中。當參數線程執行完畢後,喚醒 _WaitSet 隊列中的線程,調用 join() 方法的線程隨即恢復狀態,這也是 join() 方法保證線程運行次序的原理。

這裏我暫時列舉這麼多,關於 java 線程常見方法有問題的讀者可以點擊這裏查看我之前的博客。


參考
https://blog.csdn.net/javazejian/article/details/72828483
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章