併發Lock之AQS(AbstractQueuedSynchronizer)詳解

aoho發表於2018-01-01

1. J.U.C的lock包結構

上一篇文章講了併發程式設計的鎖機制:synchronized和lock,主要介紹了Java併發程式設計中常用的鎖機制。Lock是一個介面,而synchronized是Java中的關鍵字,synchronized是基於jvm實現。Lock鎖可以被中斷,支援定時鎖等。Lock的實現類,可重入鎖ReentrantLock,我們有講到其具體用法。而談到ReentrantLock,不得不談抽象類AbstractQueuedSynchronizer(AQS)。抽象的佇列式的同步器,AQS定義了一套多執行緒訪問共享資源的同步器框架,許多同步類實現都依賴於它,如常用的ReentrantLock、ThreadPoolExecutor。

lock
Lock包結構

2. AQS介紹

AQS是一個抽象類,主是是以繼承的方式使用。AQS本身是沒有實現任何同步介面的,它僅僅只是定義了同步狀態的獲取和釋放的方法來供自定義的同步元件的使用。AQS抽象類包含如下幾個方法:

AQS定義兩種資源共享方式:Exclusive(獨佔,只有一個執行緒能執行,如ReentrantLock)和Share(共享,多個執行緒可同時執行,如Semaphore/CountDownLatch)。共享模式時只用 Sync Queue, 獨佔模式有時只用 Sync Queue, 但若涉及 Condition, 則還有 Condition Queue。在子類的 tryAcquire, tryAcquireShared 中實現公平與非公平的區分。

不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實現共享資源state的獲取與釋放方式即可,至於具體執行緒等待佇列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在頂層實現好了。

整個 AQS 分為以下幾部分:

  • Node 節點, 用於存放獲取執行緒的節點, 存在於 Sync Queue, Condition Queue, 這些節點主要的區分在於 waitStatus 的值(下面會詳細敘述)
  • Condition Queue, 這個佇列是用於獨佔模式中, 只有用到 Condition.awaitXX 時才會將 node加到 tail 上(PS: 在使用 Condition的前提是已經獲取 Lock)
  • Sync Queue, 獨佔 共享的模式中均會使用到的存放 Node 的 CLH queue(主要特點是, 佇列中總有一個 dummy 節點, 後繼節點獲取鎖的條件由前繼節點決定, 前繼節點在釋放 lock 時會喚醒sleep中的後繼節點)
  • ConditionObject, 用於獨佔的模式, 主要是執行緒釋放lock, 加入 Condition Queue, 並進行相應的 signal 操作。
  • 獨佔的獲取lock (acquire, release), 例如 ReentrantLock。
  • 共享的獲取lock (acquireShared, releaseShared), 例如 ReeantrantReadWriteLock, Semaphore, CountDownLatch

下面我們具體來分析一下AQS實現的原始碼。

3. 內部類 Node

Node 節點是代表獲取lock的執行緒, 存在於 Condition Queue, Sync Queue 裡面, 而其主要就是 nextWaiter (標記共享還是獨佔),waitStatus 標記node的狀態。

node
內部類 Node

static final class Node {
    /** 標識節點是否是 共享的節點(這樣的節點只存在於 Sync Queue 裡面) */
    static final Node SHARED = new Node();
    //獨佔模式
    static final Node EXCLUSIVE = null;
    /**
     *  CANCELLED 說明節點已經 取消獲取 lock 了(一般是由於 interrupt 或 timeout 導致的)
     *  很多時候是在 cancelAcquire 裡面進行設定這個標識
     */
    static final int CANCELLED = 1;

    /**
     * SIGNAL 標識當前節點的後繼節點需要喚醒(PS: 這個通常是在 獨佔模式下使用, 在共享模式下有時用 PROPAGATE)
     */
    static final int SIGNAL = -1;
    
    //當前節點在 Condition Queue 裡面
    static final int CONDITION = -2;
    
    /**
     * 當前節點獲取到 lock 或進行 release lock 時, 共享模式的最終狀態是 PROPAGATE(PS: 有可能共享模式的節點變成 PROPAGATE 之前就被其後繼節點搶佔 head 節點, 而從Sync Queue中被踢出掉)
     */
    static final int PROPAGATE = -3;

    volatile int waitStatus;

    /**
     * 節點在 Sync Queue 裡面時的前繼節點(主要來進行 skip CANCELLED 的節點)
     * 注意: 根據 addWaiter方法:
     *  1. prev節點在佇列裡面, 則 prev != null 肯定成立
     *  2. prev != null 成立, 不一定 node 就在 Sync Queue 裡面
     */
    volatile Node prev;

    /**
     * Node 在 Sync Queue 裡面的後繼節點, 主要是在release lock 時進行後繼節點的喚醒
     * 而後繼節點在前繼節點上打上 SIGNAL 標識, 來提醒他 release lock 時需要喚醒
     */
    volatile Node next;

    //獲取 lock 的引用
    volatile Thread thread;

    /**
     * 作用分成兩種:
     *  1. 在 Sync Queue 裡面, nextWaiter用來判斷節點是 共享模式, 還是獨佔模式
     *  2. 在 Condition queue 裡面, 節點主要是連結且後繼節點 (Condition queue是一個單向的, 不支援併發的 list)
     */
    Node nextWaiter;

    // 當前節點是否是共享模式
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    // 獲取 node 的前繼節點
    final Node predecessor() throws NullPointerException{
        Node p = prev;
        if(p == null){
            throw new NullPointerException();
        }else{
            return p;
        }
    }

    Node(){
        // Used to establish initial head or SHARED marker
    }

    // 初始化 Node 用於 Sync Queue 裡面
    Node(Thread thread, Node mode){     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    //初始化 Node 用於 Condition Queue 裡面
    Node(Thread thread, int waitStatus){ // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}
複製程式碼

waitStatus的狀態變化:

  1. 執行緒剛入 Sync Queue 裡面, 發現獨佔鎖被其他人獲取, 則將其前繼節點標記為 SIGNAL, 然後再嘗試獲取一下鎖(呼叫 tryAcquire 方法)
  2. 若呼叫 tryAcquire 方法獲取失敗, 則判斷一下是否前繼節點被標記為 SIGNAL, 若是的話 直接 block(block前會確保前繼節點被標記為SIGNAL, 因為前繼節點在進行釋放鎖時根據是否標記為 SIGNAL 來決定喚醒後繼節點與否 <- 這是獨佔的情況下)
  3. 前繼節點使用完lock, 進行釋放, 因為自己被標記為 SIGNAL, 所以喚醒其後繼節點

waitStatus 變化過程:

  1. 獨佔模式下: 0(初始) -> signal(被後繼節點標記為release需要喚醒後繼節點) -> 0 (等釋放好lock, 會恢復到0)
  2. 獨佔模式 + 使用 Condition情況下: 0(初始) -> signal(被後繼節點標記為release需要喚醒後繼節點) -> 0 (等釋放好lock, 會恢復到0)其上可能涉及 中斷與超時, 只是多了一個 CANCELLED, 當節點變成 CANCELLED, 後就等著被清除。
  3. 共享模式下: 0(初始) -> PROPAGATE(獲取 lock 或release lock 時) (獲取 lock 時會呼叫 setHeadAndPropagate 來進行 傳遞式的喚醒後繼節點, 直到碰到 獨佔模式的節點)
  4. 共享模式 + 獨佔模式下: 0(初始) -> signal(被後繼節點標記為release需要喚醒後繼節點) -> 0 (等釋放好lock, 會恢復到0)

其上的這些狀態變化主要在: doReleaseShared , shouldParkAfterFailedAcquire 裡面。

4. Condition Queue

Condition Queue 是一個併發不安全的, 只用於獨佔模式的佇列(PS: 為什麼是併發不安全的呢? 主要是在操作 Condition 時, 執行緒必需獲取 獨佔的 lock, 所以不需要考慮併發的安全問題); 而當Node存在於 Condition Queue 裡面, 則其只有 waitStatus, thread, nextWaiter 有值, 其他的都是null(其中的 waitStatus 只能是 CONDITION, 0(0 代表node進行轉移到 Sync Queue裡面, 或被中斷/timeout)); 這裡有個注意點, 就是當執行緒被中斷或獲取 lock 超時, 則一瞬間 node 會存在於 Condition Queue, Sync Queue 兩個佇列中.

ConditionQueue
Condition Queue
節點 Node4, Node5, Node6, Node7 都是呼叫 Condition.awaitXX 方法加入 Condition Queue(PS: 加入後會將原來的 lock 釋放)。

4.1 入佇列方法 addConditionWaiter

將當前執行緒封裝成一個 Node 節點放入到 Condition Queue 裡面大家可以注意到, 下面對 Condition Queue 的操作都沒考慮到 併發(Sync Queue 的佇列是支援併發操作的), 這是為什麼呢? 因為在進行操作 Condition 是當前的執行緒已經獲取了AQS的獨佔鎖, 所以不需要考慮併發的情況。

private Node addConditionWaiter(){
    Node t = lastWaiter;                                
    // Condition queue 的尾節點           
	// 尾節點已經Cancel, 直接進行清除,
    /** 
    * 當Condition進行 awiat 超時或被中斷時, Condition裡面的節點是沒有被刪除掉的, 需要其	 * 他await 在將執行緒加入 Condition Queue 時呼叫addConditionWaiter而進而刪除, 或 await 操作差不多結束時, 呼叫 "node.nextWaiter != null" 進行判斷而刪除 (PS: 通過 signal 進行喚
    * 醒時 node.nextWaiter 會被置空, 而中斷和超時時不會)
    */
    if(t != null && t.waitStatus != Node.CONDITION){
    	/** 
    	* 呼叫 unlinkCancelledWaiters 對 "waitStatus != Node.CONDITION" 的節點進行		* 刪除(在Condition裡面的Node的waitStatus 要麼是CONDITION(正常), 要麼就是 0 
    	* (signal/timeout/interrupt))
    	*/
        unlinkCancelledWaiters();                     
        t = lastWaiter;                     
    }
    //將執行緒封裝成 node 準備放入 Condition Queue 裡面
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if(t == null){
    	//Condition Queue 是空的
        firstWaiter = node;                           
    } else {
    	// 追加到 queue 尾部
        t.nextWaiter = node;                          
    }
    lastWaiter = node;                               
    return node;
}
複製程式碼

4.2 刪除Cancelled節點的方法 unlinkCancelledWaiters

當Node在Condition Queue 中, 若狀態不是 CONDITION, 則一定是被中斷或超時。在呼叫 addConditionWaiter 將執行緒放入 Condition Queue 裡面時或 awiat 方法獲取結束時 進行清理 Condition queue 裡面的因 timeout/interrupt 而還存在的節點。這個刪除操作比較巧妙, 其中引入了 trail 節點, 可以理解為traverse整個 Condition Queue 時遇到的最後一個有效的節點。

private void unlinkCancelledWaiters(){
    Node t = firstWaiter;
    Node trail = null;
    while(t != null){
        Node next = t.nextWaiter;               // 1. 先初始化 next 節點
        if(t.waitStatus != Node.CONDITION){   // 2. 節點不有效, 在Condition Queue 裡面 Node.waitStatus 只有可能是 CONDITION 或是 0(timeout/interrupt引起的)
            t.nextWaiter = null;               // 3. Node.nextWaiter 置空
            if(trail == null){                  // 4. 一次都沒有遇到有效的節點
                firstWaiter = next;            // 5. 將 next 賦值給 firstWaiter(此時 next 可能也是無效的, 這只是一個臨時處理)
            } else {
                trail.nextWaiter = next;       // 6. next 賦值給 trail.nextWaiter, 這一步其實就是刪除節點 t
            }
            if(next == null){                  // 7. next == null 說明 已經 traverse 完了 Condition Queue
                lastWaiter = trail;
            }
        }else{
            trail = t;                         // 8. 將有效節點賦值給 trail
        }
        t = next;
    }
}
複製程式碼

4.3 轉移節點的方法 transferForSignal

transferForSignal只有在節點被正常喚醒才呼叫的正常轉移的方法。
將Node 從Condition Queue 轉移到 Sync Queue 裡面在呼叫transferForSignal之前, 會 first.nextWaiter = null;而我們發現若節點是因為 timeout / interrupt 進行轉移, 則不會進行這步操作; 兩種情況的轉移都會把 wautStatus 置為 0

final boolean transferForSignal(Node node){
    /**
     * If cannot change waitStatus, the node has been cancelled
     */
    if(!compareAndSetWaitStatus(node, Node.CONDITION, 0)){ // 1. 若 node 已經 cancelled 則失敗
        return false;
    }

    Node p = enq(node);                                 // 2. 加入 Sync Queue
    int ws = p.waitStatus;
    if(ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)){ // 3. 這裡的 ws > 0 指Sync Queue 中node 的前繼節點cancelled 了, 所以, 喚醒一下 node ; compareAndSetWaitStatus(p, ws, Node.SIGNAL)失敗, 則說明 前繼節點已經變成 SIGNAL 或 cancelled, 所以也要 喚醒
        LockSupport.unpark(node.thread);
    }
    return true;
}
複製程式碼

4.4 轉移節點的方法 transferAfterCancelledWait

transferAfterCancelledWait 在節點獲取lock時被中斷或獲取超時才呼叫的轉移方法。將 Condition Queue 中因 timeout/interrupt 而喚醒的節點進行轉移

final boolean transferAfterCancelledWait(Node node){
    if(compareAndSetWaitStatus(node, Node.CONDITION, 0)){ // 1. 沒有 node 沒有 cancelled , 直接進行轉移 (轉移後, Sync Queue , Condition Queue 都會存在 node)
        enq(node);
        return true;
    }
    
    while(!isOnSyncQueue(node)){                // 2.這時是其他的執行緒傳送signal,將本執行緒轉移到 Sync Queue 裡面的工程中(轉移的過程中 waitStatus = 0了, 所以上面的 CAS 操作失敗)
        Thread.yield();                         // 這裡呼叫 isOnSyncQueue判斷是否已經 入Sync Queue 了
    }
    return false;
}
複製程式碼

5. Sync Queue

AQS內部維護著一個FIFO的CLH佇列,所以AQS並不支援基於優先順序的同步策略。至於為何要選擇CLH佇列,主要在於CLH鎖相對於MSC鎖,他更加容易處理cancel和timeout,同時他具備進出佇列快、無所、暢通無阻、檢查是否有執行緒在等待也非常容易(head != tail,頭尾指標不同)。當然相對於原始的CLH佇列鎖,ASQ採用的是一種變種的CLH佇列鎖:

  1. 原始CLH使用的locked自旋,而AQS的CLH則是在每個node裡面使用一個狀態欄位來控制阻塞,而不是自旋。
  2. 為了可以處理timeout和cancel操作,每個node維護一個指向前驅的指標。如果一個node的前驅被cancel,這個node可以前向移動使用前驅的狀態欄位。
  3. head結點使用的是傀儡結點。

SyncQueue
Sync Queue

這個圖代表有個執行緒獲取lock, 而 Node1, Node2, Node3 則在Sync Queue 裡面進行等待獲取lock(PS: 注意到 dummy Node 的SINGNAL 這是叫獲取 lock 的執行緒在釋放lock時通知後繼節點的標示)

5.1 Sync Queue 節點入Queue方法

這裡有個地方需要注意, 就是初始化 head, tail 的節點, 不一定是 head.next, 因為期間可能被其他的執行緒進行搶佔了。將當前的執行緒封裝成 Node 加入到 Sync Queue 裡面。

private Node addWaiter(Node mode){
    Node node = new Node(Thread.currentThread(), mode);      // 1. 封裝 Node
    Node pred = tail;
    if(pred != null){                           // 2. pred != null -> 佇列中已經有節點, 直接 CAS 到尾節點
        node.prev = pred;                       // 3. 先設定 Node.pre = pred (PS: 則當一個 node在Sync Queue裡面時  node.prev 一定 != null(除 dummy node), 但是 node.prev != null 不能說明其在 Sync Queue 裡面, 因為現在的CAS可能失敗 )
        if(compareAndSetTail(pred, node)){      // 4. CAS node 到 tail
            pred.next = node;                  // 5. CAS 成功, 將 pred.next = node (PS: 說明 node.next != null -> 則 node 一定在 Sync Queue, 但若 node 在Sync Queue 裡面不一定 node.next != null)
            return node;
        }
    }
    enq(node);                                 // 6. 佇列為空, 呼叫 enq 入佇列
    return node;
}


/**
 * 這個插入會檢測head tail 的初始化, 必要的話會初始化一個 dummy 節點, 這個和 ConcurrentLinkedQueue 一樣的
 * 將節點 node 加入佇列
 * 這裡有個注意點
 * 情況:
 *      1. 首先 queue是空的
 *      2. 初始化一個 dummy 節點
 *      3. 這時再在tail後面新增節點(這一步可能失敗, 可能發生競爭被其他的執行緒搶佔)
 *  這裡為什麼要加入一個 dummy 節點呢?
 *      這裡的 Sync Queue 是CLH lock的一個變種, 執行緒節點 node 能否獲取lock的判斷通過其前繼節點
 *      而且這裡在當前節點想獲取lock時通常給前繼節點 打上 signal 的標識(表示前繼節點釋放lock需要通知我來獲取lock)
 *      若這裡不清楚的同學, 請先看看 CLH lock的資料 (這是理解 AQS 的基礎)
 */
private Node enq(final Node node){
    for(;;){
        Node t = tail;
        if(t == null){ // Must initialize       // 1. 佇列為空 初始化一個 dummy 節點 其實和 ConcurrentLinkedQueue 一樣
            if(compareAndSetHead(new Node())){  // 2. 初始化 head 與 tail (這個CAS成功後, head 就有值了, 詳情將 Unsafe 操作)
                tail = head;
            }
        }else{
            node.prev = t;                      // 3. 先設定 Node.pre = pred (PS: 則當一個 node在Sync Queue裡面時  node.prev 一定 != null, 但是 node.prev != null 不能說明其在 Sync Queue 裡面, 因為現在的CAS可能失敗 )
            if(compareAndSetTail(t, node)){     // 4. CAS node 到 tail
                t.next = node;                  // 5. CAS 成功, 將 pred.next = node (PS: 說明 node.next != null -> 則 node 一定在 Sync Queue, 但若 node 在Sync Queue 裡面不一定 node.next != null)
                return t;
            }
        }
    }
}
複製程式碼

5.2 Sync Queue 節點出Queue方法

這裡的出Queue的方法其實有兩個: 新節點獲取lock, 呼叫setHead搶佔head, 並且剔除原head;節點因被中斷或獲取超時而進行 cancelled, 最後被剔除。

/**
 * 設定 head 節點(在獨佔模式沒有併發的可能, 當共享的模式有可能)
 */
private void setHead(Node node){
    head = node;
    node.thread = null; // 清除執行緒引用
    node.prev = null; // 清除原來 head 的引用 <- 都是 help GC
}

// 清除因中斷/超時而放棄獲取lock的執行緒節點(此時節點在 Sync Queue 裡面)
private void cancelAcquire(Node node) {
    if (node == null)
        return;

    node.thread = null;                 // 1. 執行緒引用清空

    Node pred = node.prev;
    while (pred.waitStatus > 0)       // 2.  若前繼節點是 CANCELLED 的, 則也一併清除
        node.prev = pred = pred.prev;
        
    Node predNext = pred.next;         // 3. 這裡的 predNext也是需要清除的(只不過在清除時的 CAS 操作需要 它)

    node.waitStatus = Node.CANCELLED; // 4. 標識節點需要清除

    // If we are the tail, remove ourselves.
    if (node == tail && compareAndSetTail(node, pred)) { // 5. 若需要清除額節點是尾節點, 則直接 CAS pred為尾節點
        compareAndSetNext(pred, predNext, null);    // 6. 刪除節點predNext
    } else {
        int ws;
        if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL || // 7. 後繼節點需要喚醒(但這裡的後繼節點predNext已經 CANCELLED 了)
                        (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && // 8. 將 pred 標識為 SIGNAL
                pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0) // 8. next.waitStatus <= 0 表示 next 是個一個想要獲取lock的節點
                compareAndSetNext(pred, predNext, next);
        } else {
            unparkSuccessor(node); // 若 pred 是頭節點, 則此刻可能有節點剛剛進入 queue ,所以進行一下喚醒
        }

        node.next = node; // help GC
    }
}
複製程式碼

6. 獨佔Lock

6.1 獨佔方式獲取lock主要流程

  1. 呼叫 tryAcquire 嘗試性的獲取鎖(一般都是由子類實現), 成功的話直接返回
  2. tryAcquire 呼叫獲取失敗, 將當前的執行緒封裝成 Node 加入到 Sync Queue 裡面(呼叫addWaiter), 等待獲取 signal 訊號
  3. 呼叫 acquireQueued 進行自旋的方式獲取鎖(有可能會 repeatedly blocking and unblocking)
  4. 根據acquireQueued的返回值判斷在獲取lock的過程中是否被中斷, 若被中斷, 則自己再中斷一下(selfInterrupt), 若是響應中斷的則直接丟擲異常

6.2 獨佔方式獲取lock主要分成3類

  1. acquire 不響應中斷的獲取lock, 這裡的不響應中斷指的是執行緒被中斷後會被喚醒, 並且繼續獲取lock,在方法返回時, 根據剛才的獲取過程是否被中斷來決定是否要自己中斷一下(方法 selfInterrupt)
  2. doAcquireInterruptibly 響應中斷的獲取 lock, 這裡的響應中斷, 指線上程獲取 lock 過程中若被中斷, 則直接丟擲異常
  3. doAcquireNanos 響應中斷及超時的獲取 lock, 當執行緒被中斷, 或獲取超時, 則直接丟擲異常, 獲取失敗

6.3 獨佔的獲取lock 方法 acquire

acquire(int arg):以獨佔模式獲取物件,忽略中斷。

public final void acquire(int arg){
    if(!tryAcquire(arg)&&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
        selfInterrupt();
    }
}
複製程式碼
  1. 呼叫 tryAcquire 嘗試性的獲取鎖(一般都是又子類實現), 成功的話直接返回
  2. tryAcquire 呼叫獲取失敗, 將當前的執行緒封裝成 Node 加入到 Sync Queue 裡面(呼叫addWaiter), 等待獲取 signal 訊號
  3. 呼叫 acquireQueued 進行自旋的方式獲取鎖(有可能會 repeatedly blocking and unblocking)
  4. 根據acquireQueued的返回值判斷在獲取lock的過程中是否被中斷, 若被中斷, 則自己再中斷一下(selfInterrupt)。

6.4 迴圈獲取lock 方法 acquireQueued

final boolean acquireQueued(final Node node, int arg){
        boolean failed = true;
        try {
            boolean interrupted = false;
            for(;;){
                final Node p = node.predecessor();      // 1. 獲取當前節點的前繼節點 (當一個n在 Sync Queue 裡面, 並且沒有獲取 lock 的 node 的前繼節點不可能是 null)
                if(p == head && tryAcquire(arg)){       // 2. 判斷前繼節點是否是head節點(前繼節點是head, 存在兩種情況 (1) 前繼節點現在佔用 lock (2)前繼節點是個空節點, 已經釋放 lock, node 現在有機會獲取 lock); 則再次呼叫 tryAcquire嘗試獲取一下
                    setHead(node);                       // 3. 獲取 lock 成功, 直接設定 新head(原來的head可能就直接被回收)
                    p.next = null; // help GC          // help gc
                    failed = false;
                    return interrupted;                // 4. 返回在整個獲取的過程中是否被中斷過 ; 但這又有什麼用呢? 若整個過程中被中斷過, 則最後我在 自我中斷一下 (selfInterrupt), 因為外面的函式可能需要知道整個過程是否被中斷過
                }
                if(shouldParkAfterFailedAcquire(p, node) && // 5. 呼叫 shouldParkAfterFailedAcquire 判斷是否需要中斷(這裡可能會一開始 返回 false, 但在此進去後直接返回 true(主要和前繼節點的狀態是否是 signal))
                        parkAndCheckInterrupt()){      // 6. 現在lock還是被其他執行緒佔用 那就睡一會, 返回值判斷是否這次執行緒的喚醒是被中斷喚醒
                    interrupted = true;
                }
            }
        }finally {
            if(failed){                             // 7. 在整個獲取中出錯
                cancelAcquire(node);                // 8. 清除 node 節點(清除的過程是先給 node 打上 CANCELLED標誌, 然後再刪除)
            }
        }
    }
複製程式碼

主邏輯:

  1. 當前節點的前繼節點是head節點時,先 tryAcquire獲取一下鎖, 成功的話設定新 head, 返回
  2. 第一步不成功, 檢測是否需要sleep, 需要的話就sleep, 等待前繼節點在釋放lock時喚醒或通過中斷來喚醒
  3. 整個過程可能需要blocking nonblocking 幾次

6.5 支援中斷獲取lock 方法 doAcquireInterruptibly

private void doAcquireInterruptibly(int arg) throws InterruptedException{
    final Node node = addWaiter(Node.EXCLUSIVE);  // 1. 將當前的執行緒封裝成 Node 加入到 Sync Queue 裡面
    boolean failed = true;
    try {
        for(;;){
            final Node p = node.predecessor(); // 2. 獲取當前節點的前繼節點 (當一個n在 Sync Queue 裡面, 並且沒有獲取 lock 的 node 的前繼節點不可能是 null)
            if(p == head && tryAcquire(arg)){  // 3. 判斷前繼節點是否是head節點(前繼節點是head, 存在兩種情況 (1) 前繼節點現在佔用 lock (2)前繼節點是個空節點, 已經釋放 lock, node 現在有機會獲取 lock); 則再次呼叫 tryAcquire嘗試獲取一下
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return;
            }

            if(shouldParkAfterFailedAcquire(p, node) && // 4. 呼叫 shouldParkAfterFailedAcquire 判斷是否需要中斷(這裡可能會一開始 返回 false, 但在此進去後直接返回 true(主要和前繼節點的狀態是否是 signal))
                    parkAndCheckInterrupt()){           // 5. 現在lock還是被其他執行緒佔用 那就睡一會, 返回值判斷是否這次執行緒的喚醒是被中斷喚醒
                throw new InterruptedException();       // 6. 執行緒此時喚醒是通過執行緒中斷, 則直接拋異常
            }
        }
    }finally {
        if(failed){                 // 7. 在整個獲取中出錯(比如執行緒中斷)
            cancelAcquire(node);    // 8. 清除 node 節點(清除的過程是先給 node 打上 CANCELLED標誌, 然後再刪除)
        }
    }
}
複製程式碼

acquireInterruptibly(int arg): 以獨佔模式獲取物件,如果被中斷則中止。

public final void acquireInterruptibly(int arg) throws InterruptedException {    
        if (Thread.interrupted())    
            throw new InterruptedException();    
        if (!tryAcquire(arg))       
            doAcquireInterruptibly(arg);     
    }
複製程式碼

通過先檢查中斷的狀態,然後至少呼叫一次tryAcquire,返回成功。否則,執行緒在排隊,不停地阻塞與喚醒,呼叫tryAcquire直到成功或者被中斷。

6.6 超時&中斷獲取lock 方法

tryAcquireNanos(int arg, long nanosTimeout):獨佔且支援超時模式獲取: 帶有超時時間,如果經過超時時間則會退出。

private boolean doAcquireNanos(int arg, long nanosTimeout) throws InterruptedException{
    if(nanosTimeout <= 0L){
        return false;
    }

    final long deadline = System.nanoTime() + nanosTimeout; // 0. 計算截至時間
    final Node node = addWaiter(Node.EXCLUSIVE);  // 1. 將當前的執行緒封裝成 Node 加入到 Sync Queue 裡面
    boolean failed = true;

    try {
        for(;;){
            final Node p = node.predecessor(); // 2. 獲取當前節點的前繼節點 (當一個n在 Sync Queue 裡面, 並且沒有獲取 lock 的 node 的前繼節點不可能是 null)
            if(p == head && tryAcquire(arg)){  // 3. 判斷前繼節點是否是head節點(前繼節點是head, 存在兩種情況 (1) 前繼節點現在佔用 lock (2)前繼節點是個空節點, 已經釋放 lock, node 現在有機會獲取 lock); 則再次呼叫 tryAcquire嘗試獲取一下
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return true;
            }

            nanosTimeout = deadline - System.nanoTime(); // 4. 計算還剩餘的時間
            if(nanosTimeout <= 0L){                      // 5. 時間超時, 直接返回
                return false;
            }
            if(shouldParkAfterFailedAcquire(p, node) && // 6. 呼叫 shouldParkAfterFailedAcquire 判斷是否需要中斷(這裡可能會一開始 返回 false, 但在此進去後直接返回 true(主要和前繼節點的狀態是否是 signal))
                    nanosTimeout > spinForTimeoutThreshold){ // 7. 若沒超時, 並且大於spinForTimeoutThreshold, 則執行緒 sleep(小於spinForTimeoutThreshold, 則直接自旋, 因為效率更高 呼叫 LockSupport 是需要開銷的)
                LockSupport.parkNanos(this, nanosTimeout);
            }
            if(Thread.interrupted()){                           // 8. 執行緒此時喚醒是通過執行緒中斷, 則直接拋異常
                throw new InterruptedException();
            }
        }
    }finally {
        if(failed){                 // 9. 在整個獲取中出錯(比如執行緒中斷/超時)
            cancelAcquire(node);    // 10. 清除 node 節點(清除的過程是先給 node 打上 CANCELLED標誌, 然後再刪除)
        }
    }
}
複製程式碼

嘗試以獨佔模式獲取,如果中斷和超時則放棄。實現時先檢查中斷的狀態,然後至少呼叫一次tryAcquire。

public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {    
     if (Thread.interrupted())    
         throw new InterruptedException();    
     return tryAcquire(arg)|| doAcquireNanos(arg, nanosTimeout);    
}
複製程式碼

6.7 釋放lock方法

釋放 lock 流程:

  • 呼叫子類的 tryRelease 方法釋放獲取的資源
  • 判斷是否完全釋放lock(這裡有 lock 重複獲取的情況)
  • 判斷是否有後繼節點需要喚醒, 需要的話呼叫unparkSuccessor進行喚醒
public final boolean release(int arg){
    if(tryRelease(arg)){   // 1. 呼叫子類, 若完全釋放好, 則返回true(這裡有lock重複獲取)
        Node h = head;
        if(h != null && h.waitStatus != 0){ // 2. h.waitStatus !=0 其實就是 h.waitStatus < 0 後繼節點需要喚醒
            unparkSuccessor(h);   // 3. 喚醒後繼節點
        }
        return true;
    }
    return false;
}

/**
 * 喚醒 node 的後繼節點
 * 這裡有個注意點: 喚醒時會將當前node的標識歸位為 0
 * 等於當前節點標識位 的流轉過程: 0(剛加入queue) -> signal (被後繼節點要求在釋放時需要喚醒) -> 0 (進行喚醒後繼節點)
 */
private void unparkSuccessor(Node node) {
    logger.info("unparkSuccessor node:" + node + Thread.currentThread().getName());
    
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);       // 1. 清除前繼節點的標識
    Node s = node.next;
    logger.info("unparkSuccessor s:" + node + Thread.currentThread().getName());
    if (s == null || s.waitStatus > 0) {         // 2. 這裡若在 Sync Queue 裡面存在想要獲取 lock 的節點,則一定需要喚醒一下(跳過取消的節點)&emsp;(PS: s == null發生在共享模式的競爭釋放資源)
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)              // 3. 找到 queue 裡面最前面想要獲取 Lock 的節點
                s = t;
    }
    logger.info("unparkSuccessor s:"+s);
    if (s != null)
        LockSupport.unpark(s.thread);
}
複製程式碼

7. 共享Lock

7.1 共享方式獲取lock流程

  1. 呼叫 tryAcquireShared 嘗試性的獲取鎖(一般都是由子類實現), 成功的話直接返回
  2. tryAcquireShared 呼叫獲取失敗, 將當前的執行緒封裝成 Node 加入到 Sync Queue 裡面(呼叫addWaiter), 等待獲取 signal 訊號
  3. 在 Sync Queue 裡面進行自旋的方式獲取鎖(有可能會 repeatedly blocking and unblocking
  4. 當獲取失敗, 則判斷是否可以 block(block的前提是前繼節點被打上 SIGNAL 標示)
  5. 共享與獨佔獲取lock的區別主要在於 在共享方式下獲取 lock 成功會判斷是否需要繼續喚醒下面的繼續獲取共享lock的節點(及方法 doReleaseShared)

7.2 共享方式獲取lock主要分成3類

  1. acquireShared 不響應中斷的獲取lock, 這裡的不響應中斷指的是執行緒被中斷後會被喚醒, 並且繼續獲取lock,在方法返回時, 根據剛才的獲取過程是否被中斷來決定是否要自己中斷一下(方法 selfInterrupt)
  2. doAcquireSharedInterruptibly 響應中斷的獲取 lock, 這裡的響應中斷, 指線上程獲取 lock 過程中若被中斷, 則直接丟擲異常
  3. doAcquireSharedNanos 響應中斷及超時的獲取 lock, 當執行緒被中斷, 或獲取超時, 則直接丟擲異常, 獲取失敗

7.3 獲取共享lock 方法 acquireShared

public final void acquireShared(int arg){
    if(tryAcquireShared(arg) < 0){  // 1. 呼叫子類, 獲取共享 lock  返回 < 0, 表示失敗
        doAcquireShared(arg);       // 2. 呼叫 doAcquireShared 當前 執行緒加入 Sync Queue 裡面, 等待獲取 lock
    }
}
複製程式碼

7.4 獲取共享lock 方法 doAcquireShared

private void doAcquireShared(int arg){
    final Node node = addWaiter(Node.SHARED);       // 1. 將當前的執行緒封裝成 Node 加入到 Sync Queue 裡面
    boolean failed = true;

    try {
        boolean interrupted = false;
        for(;;){
            final Node p = node.predecessor();      // 2. 獲取當前節點的前繼節點 (當一個n在 Sync Queue 裡面, 並且沒有獲取 lock 的 node 的前繼節點不可能是 null)
            if(p == head){
                int r = tryAcquireShared(arg);      // 3. 判斷前繼節點是否是head節點(前繼節點是head, 存在兩種情況 (1) 前繼節點現在佔用 lock (2)前繼節點是個空節點, 已經釋放 lock, node 現在有機會獲取 lock); 則再次呼叫 tryAcquireShared 嘗試獲取一下
                if(r >= 0){
                    setHeadAndPropagate(node, r);   // 4. 獲取 lock 成功, 設定新的 head, 並喚醒後繼獲取  readLock 的節點
                    p.next = null; // help GC
                    if(interrupted){               // 5. 在獲取 lock 時, 被中斷過, 則自己再自我中斷一下(外面的函式可能需要這個引數)
                        selfInterrupt();
                    }
                    failed = false;
                    return;
                }
            }

            if(shouldParkAfterFailedAcquire(p, node) && // 6. 呼叫 shouldParkAfterFailedAcquire 判斷是否需要中斷(這裡可能會一開始 返回 false, 但在此進去後直接返回 true(主要和前繼節點的狀態是否是 signal))
                    parkAndCheckInterrupt()){           // 7. 現在lock還是被其他執行緒佔用 那就睡一會, 返回值判斷是否這次執行緒的喚醒是被中斷喚醒
                interrupted = true;
            }
        }
    }finally {
        if(failed){             // 8. 在整個獲取中出錯(比如執行緒中斷/超時)
            cancelAcquire(node);  // 9. 清除 node 節點(清除的過程是先給 node 打上 CANCELLED標誌, 然後再刪除)
        }
    }
}
複製程式碼

7.5 獲取共享lock 方法 doAcquireSharedInterruptibly

private void doAcquireSharedInterruptibly(int arg) throws InterruptedException{
    final Node node = addWaiter(Node.SHARED);            // 1. 將當前的執行緒封裝成 Node 加入到 Sync Queue 裡面
    boolean failed = true;

    try {
        for(;;){
            final Node p = node.predecessor();          // 2. 獲取當前節點的前繼節點 (當一個n在 Sync Queue 裡面, 並且沒有獲取 lock 的 node 的前繼節點不可能是 null)
            if(p == head){
                int r = tryAcquireShared(arg);          // 3. 判斷前繼節點是否是head節點(前繼節點是head, 存在兩種情況 (1) 前繼節點現在佔用 lock (2)前繼節點是個空節點, 已經釋放 lock, node 現在有機會獲取 lock); 則再次呼叫 tryAcquireShared 嘗試獲取一下
                if(r >= 0){
                    setHeadAndPropagate(node, r);       // 4. 獲取 lock 成功, 設定新的 head, 並喚醒後繼獲取  readLock 的節點
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }

            if(shouldParkAfterFailedAcquire(p, node) && // 5. 呼叫 shouldParkAfterFailedAcquire 判斷是否需要中斷(這裡可能會一開始 返回 false, 但在此進去後直接返回 true(主要和前繼節點的狀態是否是 signal))
                    parkAndCheckInterrupt()){           // 6. 現在lock還是被其他執行緒佔用 那就睡一會, 返回值判斷是否這次執行緒的喚醒是被中斷喚醒
                throw new InterruptedException();     // 7. 若此次喚醒是 通過執行緒中斷, 則直接丟擲異常
            }
        }
    }finally {
        if(failed){              // 8. 在整個獲取中出錯(比如執行緒中斷/超時)
            cancelAcquire(node); // 9. 清除 node 節點(清除的過程是先給 node 打上 CANCELLED標誌, 然後再刪除)
        }
    }
}
複製程式碼

7.6 獲取共享lock 方法 doAcquireSharedNanos

private boolean doAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException{
    if (nanosTimeout <= 0L){
        return false;
    }

    final long deadline = System.nanoTime() + nanosTimeout;  // 0. 計算超時的時間
    final Node node = addWaiter(Node.SHARED);               // 1. 將當前的執行緒封裝成 Node 加入到 Sync Queue 裡面
    boolean failed = true;

    try {
        for(;;){
            final Node p = node.predecessor();          // 2. 獲取當前節點的前繼節點 (當一個n在 Sync Queue 裡面, 並且沒有獲取 lock 的 node 的前繼節點不可能是 null)
            if(p == head){
                int r = tryAcquireShared(arg);          // 3. 判斷前繼節點是否是head節點(前繼節點是head, 存在兩種情況 (1) 前繼節點現在佔用 lock (2)前繼節點是個空節點, 已經釋放 lock, node 現在有機會獲取 lock); 則再次呼叫 tryAcquireShared 嘗試獲取一下
                if(r >= 0){
                    setHeadAndPropagate(node, r);       // 4. 獲取 lock 成功, 設定新的 head, 並喚醒後繼獲取  readLock 的節點
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
            }

            nanosTimeout = deadline - System.nanoTime(); // 5. 計算還剩餘的 timeout , 若小於0 則直接return
            if(nanosTimeout <= 0L){
                return false;
            }
            if(shouldParkAfterFailedAcquire(p, node) &&         // 6. 呼叫 shouldParkAfterFailedAcquire 判斷是否需要中斷(這裡可能會一開始 返回 false, 但在此進去後直接返回 true(主要和前繼節點的狀態是否是 signal))
                    nanosTimeout > spinForTimeoutThreshold){// 7. 在timeout 小於  spinForTimeoutThreshold 時 spin 的效率, 比 LockSupport 更高
                LockSupport.parkNanos(this, nanosTimeout);
            }
            if(Thread.interrupted()){                           // 7. 若此次喚醒是 通過執行緒中斷, 則直接丟擲異常
                throw new InterruptedException();
            }
        }
    }finally {
        if (failed){                // 8. 在整個獲取中出錯(比如執行緒中斷/超時)
            cancelAcquire(node);    // 10. 清除 node 節點(清除的過程是先給 node 打上 CANCELLED標誌, 然後再刪除)
        }
    }
}
複製程式碼

7.7 釋放共享lock

當 Sync Queue中存在連續多個獲取 共享lock的節點時, 會出現併發的喚醒後繼節點(因為共享模式下獲取lock後會喚醒近鄰的後繼節點來獲取lock)。首先呼叫子類的 tryReleaseShared來進行釋放 lock,然後判斷是否需要喚醒後繼節點來獲取 lock

private void doReleaseShared(){
    for(;;){
        Node h = head;                      // 1. 獲取 head 節點, 準備 release
        if(h != null && h != tail){        // 2. Sync Queue 裡面不為 空
            int ws = h.waitStatus;
            if(ws == Node.SIGNAL){         // 3. h節點後面可能是 獨佔的節點, 也可能是 共享的, 並且請求了喚醒(就是給前繼節點打標記 SIGNAL)
                if(!compareAndSetWaitStatus(h, Node.SIGNAL, 0)){ // 4. h 恢復  waitStatus 值置0 (為啥這裡要用 CAS 呢, 因為這裡的呼叫可能是在 節點剛剛獲取 lock, 而其他執行緒又對其進行中斷, 所用cas就出現失敗)
                    continue; // loop to recheck cases
                }
                unparkSuccessor(h);         // 5. 喚醒後繼節點
            }
            else if(ws == 0 &&
                    !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)){ //6. h後面沒有節點需要喚醒, 則標識為 PROPAGATE 表示需要繼續傳遞喚醒(主要是區別 獨佔節點最終狀態0 (獨佔的節點在沒有後繼節點, 並且release lock 時最終 waitStatus 儲存為 0))
                continue; // loop on failed CAS // 7. 同樣這裡可能存在競爭
            }
        }

        if(h == head){ // 8. head 節點沒變化, 直接 return(從這裡也看出, 一個共享模式的 節點在其喚醒後繼節點時, 只喚醒一個, 但是它會在獲取 lock 時喚醒, 釋放 lock 時也進行, 所以或導致競爭的操作)
            break;           // head 變化了, 說明其他節點獲取 lock 了, 自己的任務完成, 直接退出
        }

    }
}
複製程式碼

8. 總結

本文主要講過了抽象的佇列式的同步器AQS的主要方法和實現原理。分別介紹了Node、Condition Queue、 Sync Queue、獨佔獲取釋放lock、共享獲取釋放lock的具體原始碼實現。AQS定義了一套多執行緒訪問共享資源的同步器框架,許多同步類實現都依賴於它。

訂閱最新文章,歡迎關注我的公眾號

微信公眾號

參考

  1. Java併發之AQS詳解
  2. AbstractQueuedSynchronizer 原始碼分析 (基於Java 8)

相關文章