【JDK1.8】JUC——AbstractQueuedSynchronizer

quxing10086發表於2018-05-07

目錄

一、前言
二、結構概覽
三、原始碼閱讀
3.1 tryAcquire(int arg)
3.2 addWaiter(Node mode)
3.3 acquireQueued(final Node node, int arg)
3.4 cancelAcquire(Node node)
3.5 release(int arg)
四、總結

一、前言

上一篇中,我們對LockSupport進行了閱讀,因為它是實現我們今天要分析的AbstractQueuedSynchronizer(簡稱AQS)的基礎,重新用一下最開始的圖:

juc

可以看到,在ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock中都用到了繼承自AQS的Sync內部類,正如AQS的java doc中一開始描述:

Provides a framework for implementing blocking locks and related synchronizers (semaphores, events, etc) that rely on first-in-first-out (FIFO) wait queues.

為實現依賴於先進先出(FIFO)等待佇列的阻塞鎖和相關同步器(訊號量,事件等)提供框架。

AQS根據模式的不同:獨佔(EXCLUSIVE)和共享(SHARED)模式。

  • 獨佔:只有一個執行緒能執行。如ReentrantLock。
  • 共享:多個執行緒可同時執行。如Semaphore,可以設定指定數量的執行緒共享資源。

對應的類根據不同的模式,來實現對應的方法。


二、結構概覽

試想一下鎖的應用場景,當執行緒試圖請求資源的時候,先呼叫lock,如果獲得鎖,則得以繼續執行,而沒有獲得,則排隊阻塞,直到鎖被其他執行緒釋放,聽起來就像是一個列隊的結構。而實際上AQS底層就是一個先進先出的等待佇列

佇列採用了連結串列的結構,node作為基本結構,主要有以下幾個成員變數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static final class Node {
    //用來表明當前節點的等待狀態,主要有下面幾個:
    // CANCELLED: 1, 表示當前的執行緒被取消
    // SIGNAL: -1, 表示後繼節點需要執行,也就是unpark
    // CONDITION: -2, 表示執行緒在等待condition
    // PROPAGATE: -3, 表示後續的acquireShared能夠得以執行,在共享模式中用到,後面會說
    // 0, 初始狀態,在佇列中等待
    volatile int waitStatus;
    // 指向前一個node
    volatile Node prev;
    // 指向後一個node
    volatile Node next;
    // 指向等待的那個執行緒
    volatile Thread thread;
    // 在condition中用到
    Node nextWaiter;
}

在AQS中,用head,tail來記錄了佇列的頭和尾,方便快速操作佇列:

1
2
3
4
5
6
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
    private transient volatile Node head;
    private transient volatile Node tail;
    // 同步狀態
    private volatile int state;
}

AQS的基本框架就是:state作為同步資源狀態,當執行緒請求鎖的時候,根據state數值判斷能否獲得鎖。不能,則加入佇列中等待。當持有鎖的執行緒釋放的時候,根據佇列裡的順序來決定誰先獲得鎖。


三、原始碼閱讀

獨佔模式典型的實現就是ReentrantLock,其具體流程如下:

獨佔模式下對應的lock-unlock就是acquire-release。整個過程如上圖所示。我們先來看一下acquire方法:

1
2
3
4
5
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  1. 呼叫tryAcquire(),該方法會在獨佔模式下嘗試請求獲取物件狀態。具體的實現由實現類去決定。
  2. 如果tryAcquire()失敗,即返回false,則呼叫addWaiter函式,將當前執行緒標記為獨佔模式,加入佇列的尾部。
  3. 呼叫acquireQueued(),讓執行緒在佇列中等待獲取資源,一直獲取到資源後才返回。如果在等阿迪過程中被中斷過,則返回true,否則返回false
  4. 如果執行緒被中斷過,在獲取鎖之後,呼叫中斷


3.1 tryAcquire(int arg)

下面來具體看一下各個方法:

1
2
3
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

前面說過了,AQS提供的是框架,其具體的實現由實現類來完成,tryAcquire就是其中之一,需要子類自己實現的方法,那既然要自己實現,為什麼不加abstract關鍵字,因為前面提到過,只有獨佔模式的實現類才需要實現這個方法,像Semaphore,CountDownLatch等共享模式的類不需要用到這個方法。如果加了關鍵字,那麼這些類還要實現,顯得很雞肋。


3.2 addWaiter(Node mode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Node addWaiter(Node mode) {
    // 將當前執行緒封裝進node
    Node node = new Node(Thread.currentThread(), mode);
     
    Node pred = tail;
    // 插入佇列尾部,並維持節點前後關係
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 上一步如果失敗,在enq中繼續處理
    enq(node);
    return node;
}

邏輯相對簡單,其中compareAndSetTail採用Unsafe類來實現。那麼下面的enq()方法是具體做了什麼呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        // 佇列初始化
        if (t == null) {
            if (compareAndSetHead(new Node()))
                tail = head;
        // 重複執行插入直到return
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

enq()方法為了防止在addWaiter中,節點插入佇列失敗沒有return,或者佇列沒有初始化,在for迴圈中反覆執行,確保插入成功,返回節點。


3.3 acquireQueued(final Node node, int arg)

到目前為止,走到acquireQueued()呼叫了前兩個方法,意味著獲取資源失敗,將節點加入了等待佇列,那麼下面要做的就是阻塞當前的執行緒,等待資源被是否後,再次喚醒執行緒來取得資源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 獲取當前節點的前一個節點
            final Node p = node.predecessor();
            // 前一個節點是頭結點,且獲取到了資源
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 不符合上面的條件,那麼只能被park,等待被喚醒
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

acquireQueued當中,用for迴圈來讓執行緒等待,直至獲得資源return。而return的條件就是當前的節點是第二個節點,且頭結點已經釋放了資源。

再來看看shouldParkAfterFailedAcquire和parkAndCheckInterrupt方法

先來說一下parkAndCheckInterrupt:

1
2
3
4
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

呼叫LockSupport.park,阻塞當前執行緒,當執行緒被重新喚醒後,返回是否被中斷過。

再來重點看一下shouldParkAfterFailedAcquire:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 獲取前一個節點的狀態
    int ws = pred.waitStatus;
    // 如果前一個節點的狀態是signal,前面提到表明會unpark下一個節點,則true
    if (ws == Node.SIGNAL)
        return true;
    // 如果ws > 0 即CANCELLED,則向前找,直到找到正常狀態的節點。
    if (ws > 0) {
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        // 維護正常狀態
        pred.next = node;
    // 將前一個節點設定為SIGNAL
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

shouldParkAfterFailedAcquire的主要作用就是將node放置在SIGNAL狀態的前節點下,確保能被喚醒,在呼叫該方法後,CANCELLED狀態的節點因為沒有引用執行它將被GC。

那麼問題來了,什麼時候節點會被設定為CANCELLED狀態

答案就在try-finally的cancelAcquire(node)當中。當在acquireQueued取鎖的過程中,丟擲了異常,則會呼叫cancelAcquire。將當前節點的狀態設定為CANCELLED。


3.4 cancelAcquire(Node node)

我們先來看一下它的原始碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
private void cancelAcquire(Node node) {
    // node為空,啥都不幹
    if (node == null)
        return;
    node.thread = null;
 
    // while查詢,直到找到非CANCELLED的節點
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
 
    // 獲取非CANCELLED的節點的下一個節點,predNext肯定是CANCELLED
    Node predNext = pred.next;
 
    // 設定當前節點為CANCELLED狀態
    node.waitStatus = Node.CANCELLED;
 
    // 如果節點在佇列尾部,直接移除自己就可以了
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        int ws;
        // 重新維護剩下的連結串列關係
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            // 喚醒node的下一個節點
            unparkSuccessor(node);
        }
        // help GC
        node.next = node;
    }
}

總結來說,cancelAcquire就是用來維護連結串列正常狀態的關係,直接看程式碼認識起來可能還比較模糊,放圖:

幾個注意點:

  1. 如果node為第二個節點的時候,pred == head,喚醒下一個節點next_node,next_node執行緒會繼續在acquireQueued的for迴圈中執行,呼叫shouldParkAfterFailedAcquire會重新維護狀態,排除node節點
  2. 呼叫if裡的邏輯後,可以看到next的prev還指向node,會導致node無法被gc,這一點不用擔心,當next呼叫setHead被設定為head的時候,next的prev會被設定為null,這樣node就會被gc
1
2
3
4
5
private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

以上部分就是acquire的所有部分,建議忘記的園友們可以回到上面重新看一下流程圖,再接著穩固一遍。


3.5 release(int arg)

下面開始release的原始碼解析,相對於acquire來說要簡單一些:

1
2
3
4
5
6
7
8
9
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

與acquire一樣,tryRelease由實現類自己實現,如果為true,則unpark佇列頭部的下一個節點。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void unparkSuccessor(Node node) {
    // 清楚小於0的狀態
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
 
    // 如果下一個節點是CANCELLED,則從尾部向頭部找距離node最近的非CANCELLED節點
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // unpark找到的節點
    if (s != null)
        LockSupport.unpark(s.thread);
}

至此acuqire-release的部分就此結束了,至於共享模式的程式碼大同小異,在後面分析訊號量的時候會再提及~


四、總結

AQS應該是整個JUC中各個類涉及最多的了,其重要性可想而知,在瞭解其實現原理後,有助於我們分析其他的程式碼。最後謝謝各位園友觀看,如果有描述不對的地方歡迎指正,與大家共同進步!

http://www.wgt9662.top/
http://www.bux1348.top/
http://www.ukr4854.top/
http://www.cfs8763.top/
http://www.psd1092.top/
http://www.xck1603.top/
http://www.fgm4024.top/
http://www.zoj1707.top/
http://www.oiv1998.top/
http://www.ftw8814.top/
http://www.jfs6888.top/
http://www.kdx4817.top/
http://www.sbx6519.top/
http://www.rrq5611.top/
http://www.pxk9336.top/
http://www.vik6796.top/
http://www.kod8371.top/
http://www.nuq3623.top/
http://www.vfv3740.top/
http://www.tbt7039.top/
http://www.wky3695.top/
http://www.kcs3342.top/
http://www.gum4900.top/
http://www.mrw5927.top/
http://www.wnu1861.top/
http://www.vlc4617.top/
http://www.idv6045.top/
http://www.jmk5203.top/
http://www.mug5965.top/
http://www.gtt6107.top/
http://www.cnp6436.top/
http://www.sdx1013.top/
http://www.jwd3113.top/
http://www.qeu2095.top/
http://www.tux4376.top/
http://www.tay3928.top/
http://www.tgq6935.top/
http://www.win4778.top/
http://www.ngh4321.top/
http://www.cqq1459.top/
http://www.fxm1291.top/
http://www.wyz5825.top/
http://www.kbx3827.top/
http://www.tqt8862.top/
http://www.yma5505.top/
http://www.rye6349.top/
http://www.xqs4260.top/
http://www.fjv3790.top/
http://www.wqv1289.top/
http://www.mxz6626.top/
http://www.npl2536.top/
http://www.vme0237.top/
http://www.edr0603.top/
http://www.kft8502.top/
http://www.kwb2561.top/
http://www.dqv1869.top/
http://www.iai9521.top/
http://www.jla2696.top/
http://www.vip1477.top/

相關文章