在并發多執行緒的情況下,為了保證資料安全性,一般我們會對資料進行加鎖,通常使用Synchronized或者ReentrantLock同步鎖,Synchronized是基于JVM實作,而ReentrantLock是基于Java代碼層面實作的,底層是繼承的AQS,
AQS全稱AbstractQueuedSynchronizer,即抽象佇列同步器,是一種用來構建鎖和同步器的框架,
我們常見的并發鎖ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier都是基于AQS實作的,所以說不懂AQS實作原理的,就不能說了解Java鎖,
當我仔細研究AQS底層加鎖原理,發現竟然跟Synchronized加鎖原理有驚人的相似,讓我突然想到一句名言,記不清怎么說了,意思是框架底層原理很相似,大家多學習底層原理,
Synchronized的加鎖流程在前幾篇文章已經詳細講過,沒看過一塊再溫習一下,
1. Synchronized加鎖流程
我們先想一下Synchronized的加鎖需求,如果讓你設計Synchronized的物件鎖存盤結構,該怎么設計?
- 多個執行緒執行到Synchronized代碼塊,只有一個執行緒獲取鎖,然后執行同步代碼塊(需要記錄哪個執行緒獲取了物件鎖),
- 其他執行緒被阻塞(被阻塞的執行緒,是不是可以用鏈表設計個阻塞佇列?)
- 持有鎖的執行緒呼叫wait方法,釋放鎖,等待被喚醒(等待的執行緒,是不是可以用鏈表設計個等待佇列?),
- 被阻塞的執行緒開始競爭鎖
- 呼叫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 ;
}

上圖展示了物件鎖的基本作業機制:
-
當多個執行緒同時訪問一段同步代碼時,首先會進入 _EntryList佇列中阻塞,
-
當某個執行緒獲取到物件的物件鎖后進入臨界區域,并把物件鎖中的 _owner變數設定為當前執行緒,即獲得物件鎖,
-
若持有物件鎖的執行緒呼叫 wait() 方法,將釋放當前持有的物件鎖,_owner變數恢復為null,同時該執行緒進入 _WaitSet 集合中等待被喚醒,
-
在_WaitSet集合中的執行緒被喚醒,會被再次放到_EntryList佇列中,重新競爭獲取鎖,
-
若當前執行緒執行完畢也將釋放物件鎖并復位變數的值,以便其他執行緒進入獲取鎖,
Synchronized物件鎖存盤結構和加鎖流程,竟然跟咱們想的一樣,
再看一下AQS的存盤結構和加鎖流程,有沒有相似的地方,
2. AQS加鎖原理
先分析一下,我們使用AQS的加鎖需求:
- 多個執行緒執行到acquire方法的時候,只有一個執行緒獲取鎖,然后執行同步代碼塊(需要記錄哪個執行緒獲取了物件鎖),
- 其他執行緒被阻塞(被阻塞的執行緒,是不是可以用鏈表設計個阻塞佇列?名叫”同步佇列“?)
- 持有鎖的執行緒呼叫await方法,釋放鎖,等待被喚醒(等待的執行緒,是不是可以用鏈表設計個等待佇列?名叫”條件佇列“?),
- 被阻塞的執行緒開始競爭鎖
- 呼叫signal方法,喚醒等待的執行緒,被喚醒的執行緒進入阻塞佇列,一塊競爭鎖,
AQS的需求跟Synchronized一模一樣,
我們再看一下AQS實際的加鎖機制是怎么設計的?是不是跟Synchronized相似?

AQS的加鎖流程并不復雜,只要理解了同步佇列和條件佇列,以及它們之間的資料流轉,就算徹底理解了AQS,
- 當多個執行緒競爭AQS鎖時,如果有個執行緒獲取到鎖,就把ower執行緒設定為自己
- 沒有競爭到鎖的執行緒,在同步佇列中阻塞(同步佇列采用雙向鏈表,尾插法),
- 持有鎖的執行緒呼叫await方法,釋放鎖,追加到條件佇列的末尾(條件佇列采用單鏈表,尾插法),
- 持有鎖的執行緒呼叫signal方法,喚醒條件佇列的頭節點,并轉移到同步佇列的末尾,
- 同步佇列的頭節點優先獲取到鎖
可以看到AQS和Synchronized的加鎖流程幾乎是一模一樣的,AQS中同步佇列就是Synchronized中EntryList,AQS中條件佇列就是Synchronized中的waitSet,兩個佇列之間的資料轉移流程也是一樣的,
3. 總結
AQS跟Synchronized的加鎖流程是一樣的,都是通過同步佇列和條件佇列實作的,阻塞狀態的執行緒被放到同步佇列中,等待狀態的執行緒被放到條件佇列中,從條件佇列喚醒的執行緒又被轉移到同步佇列末尾,一塊競爭鎖,
看完AQS加鎖流程,還沒有人不懂AQS的?
下篇文章再講一下AQS加鎖具體的原始碼實作,里面有很多精巧的設計,值得我們學習,
比如:
為什么同步佇列要設計成雙向鏈表?而條件佇列要設計成單鏈表?
為什么AQS加鎖性能這么好(樂觀鎖CAS使用)?
同步佇列和條件佇列中節點怎么用一個物件實作?
釋放鎖后,怎么喚醒同步佇列中執行緒?
我是「一燈架構」,如果本文對你有幫助,歡迎各位小伙伴點贊、評論和關注,感謝各位老鐵,我們下期見

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/530507.html
標籤:Java
下一篇:java--IO流 ? 位元組流
