AQS系列(七)- 終篇:AQS總結

淡墨痕發表於2020-04-19

前言

本文是對之前AQS系列文章的一個小結,首先看看以下幾個問題:

1、ReentrantLock和ReentrantReadWriteLock的可重入特性是如何實現的?

2、哪個變數控制著鎖是否被佔用?

3、多個執行緒競爭一個排它鎖時,未搶到鎖的執行緒是如何阻塞的?

4、讀讀真的可以一直共享不阻塞嗎?

對於以上問題,你是否都能知道答案 ?是否都清楚其原理?如果是的話,就沒必要閱讀本文了,否則還請慢慢讀來。

 

正文

1、可重入性的實現(針對排它鎖)-問題1

    可重入性是通過exclusiveOwnerThread和state一起實現的。在AQS的父類AbstractOwnableSynchronizer中有一個成員變數exclusiveOwnerThread來存放獲取到獨佔鎖時的執行緒,可重入的特性就是通過此變數實現的。如果根據state判斷當前鎖已被佔用,那麼再判斷這個變數中存的是不是當前執行緒,如果是則獲取到鎖,鎖計數+1,否則獲取不到鎖。(此處針對的是排它鎖,共享鎖不屬於這個範疇)

2、AQS中state的值-問題2

    state為volatile int型別,用於記錄加鎖狀態和重入次數,但針對不同的實現類其記錄方式又不同,下面分別進行說明:

【ReentrantLock】state用於記錄鎖的持有狀態和重入次數,state=0表示沒有執行緒持有鎖;state=1表示有一個執行緒持有鎖;state=N表示exclusiveOwnerThread這個執行緒N次重入了這個鎖。

【ReentrantReadWriteLock】state用於記錄讀寫鎖的佔用狀態和持有執行緒數量(讀鎖)、重入次數(寫鎖),state的高16位記錄持有讀鎖的執行緒數量,低16位記錄寫鎖執行緒重入次數,如果這16位的值是0,表示沒有執行緒佔用鎖,否則表示有執行緒持有鎖。另外針對讀鎖,每個執行緒獲取到的讀鎖次數由本地執行緒變數中的HoldCounter記錄。

【Semaphore】state用於計數。state=N表示還有N個訊號量可以分配出去,state=0表示沒有訊號量了,此時所有需要acquire訊號量的執行緒都等著;

【CountDownLatch】:state也用於計數,每次countDown都減一,減到0的時候喚醒被await阻塞的執行緒。

 

3、關於Node中waitStatus的值

waitStatus為volatile int 型別,有5種值,作用分別為:

1: CANCEL,即取消狀態,這種狀態的節點是無效節點,在執行時會直接略過,一般只有在特殊的異常場景中才會出現這種狀態;

0:初始化狀態,新建的Node節點都是這個狀態;

-1:SIGNAL,即可喚醒狀態,如果一個節點加鎖失敗需進入阻塞狀態,必須先將它的前一個節點置為-1,自己才會進入park狀態,在AQS中搜parkAndCheckInterrupt()可以發現,只要是這個方法出現的地方,前面一定有shouldParkAfterFailedAcquire(p, node)方法先將p的waitStatus置為-1;

-2:CONDITION,跟條件佇列相關的狀態,ReentrantReadWriteLock和ReentrantLock中暫未涉及;

-3:PROPAGATE,可傳播狀態,沒看出來有什麼用處。

 

4、關於head節點和tail節點

當第一個執行緒過來獲取鎖時,是不會初始化佇列的,只是將exclusiveOwnerThread這個變數變為當前執行緒(獨佔鎖的情況下),此時head和tail都是null;

第一個執行緒沒執行完,第二個執行緒又來了,這時會初始化佇列,new一個空Node賦值給head和tail,然後在tail(也就是head)後面拼上這個要排隊的執行緒(詳見下面的enq方法),此時形成了一個有兩個節點的雙向佇列,head是new Node(),tail是新加入需排隊的Node;然後再獲取不到鎖就會將head的waitStatus置為-1,自身掛起,等待unparkSuccessor(head)來喚醒。

addWaiter方法:如果tail不為空,則將當前的node節點賦值給tail,原先的tail變為新tail在連結串列上的前置節點。由此可知此連結串列的新增方式是後入式。這也解釋了為什麼喚醒執行緒時是從tail往前遍歷找排在最前面符合條件的node節點。

 1 private Node addWaiter(Node mode) {
 2         Node node = new Node(Thread.currentThread(), mode);
 3         // Try the fast path of enq; backup to full enq on failure
 4         Node pred = tail;
 5         if (pred != null) {
 6             node.prev = pred;
 7             if (compareAndSetTail(pred, node)) {
 8                 pred.next = node;
 9                 return node;
10             }
11         }
12         enq(node);
13         return node;
14     }

在AQS類中的enq方法程式碼如下:跟addWaiter類似,只是多了一步初始化head/tail的步驟。

 1 private Node enq(final Node node) {
 2         for (;;) {
 3             Node t = tail;
 4             if (t == null) { // Must initialize
 5                 if (compareAndSetHead(new Node())) // 初始化頭節點,這時head指標指向的就是new Node()物件
 6                     tail = head; // 將tail也一起初始化了,這時會繼續再走一遍for迴圈
 7             } else {
 8                 node.prev = t; // 先將node的prev指標指向區域性變數t;
 9                 if (compareAndSetTail(t, node)) { // 將tail所在記憶體地址的物件值換成node
10                     t.next = node; // 將tail的next指標指向node節點,這時node節點就成了新的隊尾了
11                     return t; // 返回舊的隊尾t
12                 }
13             }
14         }
15     }

 

4、關於讀讀鎖的有條件共享問題-問題4

ReentrantReadWriteLock中,讀鎖和讀鎖並不是永遠都可以同時進行,如果當前執行的是讀鎖,而後面第一個排隊的是寫鎖,那麼再來新的讀鎖會進入連結串列排隊阻塞而不是直接獲取讀鎖,因為不這樣設定則可能存在由於一直有讀鎖過來導致後面的寫鎖總是處於阻塞狀態獲取不到鎖。

可參見readShouldBlock方法的非公平讀鎖實現-apparentlyFirstQueuedIsExclusive()方法

1 final boolean apparentlyFirstQueuedIsExclusive() {
2         Node h, s;
3         return (h = head) != null &&
4             (s = h.next)  != null &&
5             !s.isShared()         &&
6             s.thread != null;
7     }

 

5、Object的wait/notify和LockSupport的park/unpark方法-問題3

LockSupport對執行緒的喚醒和掛起,是基於Thread直接訊號量進行的,而wait/notify是基於物件的,需要依賴於Monitor物件。

park/unpark的語義更容易理解,park是針對當前執行緒阻塞,unpark是針對指定執行緒喚醒,不像wait/notify那樣違反常識。

unpark為指定執行緒提供一個許可,有這個許可執行緒就能繼續執行;而park是等待一個許可。unpark可以在park之前執行,執行緒不會阻塞。但unpark不能重入疊加,多個unpark也會被一個park直接用掉。

在底層,UNSAFE的park/unpark方法操作的是Paker物件(每個執行緒都有一個Parker例項),物件中維護了一個_counter變數,呼叫unpark方法後變數從0變為1,表示擁有繼續執行的許可,執行之後恢復為0,呼叫park方法後變數如果不是1則執行緒阻塞,直到變為1再喚醒繼續執行。

 

關於AQS暫時就寫這些,後面還會針對JUC包裡的其他工具類進行學習。

相關文章