重大發現,AQS加鎖機制竟然跟Synchronized有驚人的相似

在並發多執行緒的情況下,為了保證數據安全性,一般我們會對數據進行加鎖,通常使用Synchronized或者ReentrantLock同步鎖。Synchronized是基於JVM實現,而ReentrantLock是基於Java程式碼層面實現的,底層是繼承的AQS。

AQS全稱AbstractQueuedSynchronizer,即抽象隊列同步器,是一種用來構建鎖和同步器的框架。

我們常見的並發鎖ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier都是基於AQS實現的,所以說不懂AQS實現原理的,就不能說了解Java鎖。

當我仔細研究AQS底層加鎖原理,發現竟然跟Synchronized加鎖原理有驚人的相似。讓我突然想到一句名言,記不清怎麼說了,意思是框架底層原理很相似,大家多學習底層原理。

Synchronized的加鎖流程在前幾篇文章已經詳細講過,沒看過一塊再溫習一下。

1. Synchronized加鎖流程

我們先想一下Synchronized的加鎖需求,如果讓你設計Synchronized的對象鎖存儲結構,該怎麼設計?

  1. 多個執行緒執行到Synchronized程式碼塊,只有一個執行緒獲取鎖,然後執行同步程式碼塊(需要記錄哪個執行緒獲取了對象鎖)。
  2. 其他執行緒被阻塞(被阻塞的執行緒,是不是可以用鏈表設計個阻塞隊列?)
  3. 持有鎖的執行緒調用wait方法,釋放鎖,等待被喚醒(等待的執行緒,是不是可以用鏈表設計個等待隊列?)。
  4. 被阻塞的執行緒開始競爭鎖
  5. 調用notify方法,喚醒等待的執行緒,被喚醒的執行緒進入阻塞隊列,一塊競爭鎖。

上面描述了Synchronized的加鎖流程,Synchronized的對象鎖存儲結構是不是跟咱們想的一樣?實際就是的。

下面是對象鎖的存儲數據結構(由C++實現):

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL; // 持有鎖的執行緒
    _WaitSet      = NULL; // 等待隊列,存儲處於wait狀態的執行緒
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 阻塞隊列,存儲處於等待鎖block狀態的執行緒
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

image

上圖展示了對象鎖的基本工作機制:

  1. 當多個執行緒同時訪問一段同步程式碼時,首先會進入 _EntryList隊列中阻塞。

  2. 當某個執行緒獲取到對象的對象鎖後進入臨界區域,並把對象鎖中的 _owner變數設置為當前執行緒,即獲得對象鎖。

  3. 若持有對象鎖的執行緒調用 wait() 方法,將釋放當前持有的對象鎖,_owner變數恢復為null,同時該執行緒進入 _WaitSet 集合中等待被喚醒。

  4. 在_WaitSet集合中的執行緒被喚醒,會被再次放到_EntryList隊列中,重新競爭獲取鎖。

  5. 若當前執行緒執行完畢也將釋放對象鎖並複位變數的值,以便其他執行緒進入獲取鎖。

Synchronized對象鎖存儲結構和加鎖流程,竟然跟咱們想的一樣。

再看一下AQS的存儲結構和加鎖流程,有沒有相似的地方。

2. AQS加鎖原理

先分析一下,我們使用AQS的加鎖需求:

  1. 多個執行緒執行到acquire方法的時候,只有一個執行緒獲取鎖,然後執行同步程式碼塊(需要記錄哪個執行緒獲取了對象鎖)。
  2. 其他執行緒被阻塞(被阻塞的執行緒,是不是可以用鏈表設計個阻塞隊列?名叫」同步隊列「?)
  3. 持有鎖的執行緒調用await方法,釋放鎖,等待被喚醒(等待的執行緒,是不是可以用鏈表設計個等待隊列?名叫」條件隊列「?)。
  4. 被阻塞的執行緒開始競爭鎖
  5. 調用signal方法,喚醒等待的執行緒,被喚醒的執行緒進入阻塞隊列,一塊競爭鎖。

AQS的需求跟Synchronized一模一樣。

我們再看一下AQS實際的加鎖機制是怎麼設計的?是不是跟Synchronized相似?

image

AQS的加鎖流程並不複雜,只要理解了同步隊列和條件隊列,以及它們之間的數據流轉,就算徹底理解了AQS。

  1. 當多個執行緒競爭AQS鎖時,如果有個執行緒獲取到鎖,就把ower執行緒設置為自己
  2. 沒有競爭到鎖的執行緒,在同步隊列中阻塞(同步隊列採用雙向鏈表,尾插法)。
  3. 持有鎖的執行緒調用await方法,釋放鎖,追加到條件隊列的末尾(條件隊列採用單鏈表,尾插法)。
  4. 持有鎖的執行緒調用signal方法,喚醒條件隊列的頭節點,並轉移到同步隊列的末尾。
  5. 同步隊列的頭節點優先獲取到鎖

可以看到AQS和Synchronized的加鎖流程幾乎是一模一樣的,AQS中同步隊列就是Synchronized中EntryList,AQS中條件隊列就是Synchronized中的waitSet,兩個隊列之間的數據轉移流程也是一樣的。

3. 總結

AQS跟Synchronized的加鎖流程是一樣的,都是通過同步隊列和條件隊列實現的,阻塞狀態的執行緒被放到同步隊列中,等待狀態的執行緒被放到條件隊列中,從條件隊列喚醒的執行緒又被轉移到同步隊列末尾,一塊競爭鎖。

看完AQS加鎖流程,還沒有人不懂AQS的?

下篇文章再講一下AQS加鎖具體的源碼實現。裡面有很多精巧的設計,值得我們學習。

比如:

為什麼同步隊列要設計成雙向鏈表?而條件隊列要設計成單鏈表?

為什麼AQS加鎖性能這麼好(樂觀鎖CAS使用)?

同步隊列和條件隊列中節點怎麼用一個對象實現?

釋放鎖後,怎麼喚醒同步隊列中執行緒?

我是「一燈架構」,如果本文對你有幫助,歡迎各位小夥伴點贊、評論和關注,感謝各位老鐵,我們下期見

image