執行緒系列四AQS

心無私天地寬發表於2019-01-19

1、什麼是aqs

aqs是一個FIFO的雙向連結串列佇列。aqs將等待獲取鎖的執行緒封裝成結點,放在佇列中。

我們可以將aqs的作用理解為在多執行緒的環境下保證執行緒等待獲取鎖(新增進入佇列)以及執行緒獲取鎖,並佇列中出去都是執行緒安全的。

更簡單的可以理解為aqs為了保證在多執行緒的環境下入佇列出佇列執行緒安全性提供了一個基本功能框架。


2、aqs是如何做到執行緒安全的

aqs主要是通過cas + 死迴圈以及state狀態值,來做到執行緒安全。


3、aqs為什麼會被設計為FIFO雙向連結串列佇列(以下是個人理解)
①aqs的鎖實現,包含公平鎖和非公平鎖。為了實現公平鎖,必須使用佇列來保證獲取鎖的順序(入佇列的順序)

②用連結串列的方式,主要是因為,操作更多是刪除與增加。連結串列時間複雜度O(1)的效率會比陣列O(n)的低。

③用雙向佇列的原因是,aqs的設計思想,或則說為了解決羊群效應(為了爭奪鎖,大量執行緒同時被喚醒)。每個結點(執行緒)只需要關心自己的前一個結點的狀態(後續會說),執行緒喚醒也只喚醒隊頭等待執行緒

請參考 http://www.importnew.com/2400…


4、aqs是如何提供一個基礎框架的

aqs 通過模板設計進行提供的,實現類只需實現特定的方法即可。

以下是aqs的模板方法

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

。。。 其他的省略了

protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

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

tryAcquire(int arg)tryRelease(int arg) 是我們要實現的模板方法,當然還有分享鎖的,這裡只介紹了獨佔鎖的。


5、從原始碼角度剖析aqs。aqs是如何通過雙向連結串列佇列,cas,state狀態值,以及結點狀態來保證入佇列出佇列的執行緒安全的!

注:以下只介紹獨佔式的不公平鎖

①aqs 如何獲取鎖?

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

tryAcquire(arg) 內部呼叫了nonfairTryAcquire(int acquires)

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {  // 鎖未被獲取
         // cas(自旋) 獲取鎖,並修改state 狀態值
        if (compareAndSetState(0, acquires)) {
         // 設定當前佔有的執行緒
            setExclusiveOwnerThread(current);
            return true;
        }
    }  //  重入
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

解釋:利用cas自旋式的獲取鎖。


②aqs 獲取鎖失敗,如何處理?

在看程式碼前,先解釋一下:將當前執行緒包裝成Node結點,並插入同步佇列中,並用CAS形式嘗試獲取鎖,獲取失敗,則掛起當前執行緒(以上只是說了大概)

先看第1個方法(將當前執行緒包裝成Node結點,並插入同步佇列)

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {  // 尾節點不為空
        node.prev = pred;
        // 用 CAS 將當前執行緒插入隊尾
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 尾節點為空,說明當前佇列還是空的,需要初始化
    enq(node);
    return node;
}

private Node enq(final Node node) {
    // 死迴圈
    for (;;) {
        // 初始化
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else { // 這裡主要是擔心有多個執行緒同時進到enq(final Node node) 方法
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

解釋:佇列若為空,先初始化,不為空,用 CAS 將當前結點插入到隊尾

再看第二個方法final boolean acquireQueued(final Node node, int arg);

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;
            }
            // 前置結點不是頭結點 或者 前置結點是頭結點但是嘗試獲取鎖失敗
            // 則,應當將當前執行緒掛起(畢竟不能一直死迴圈獲取吧~)
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}


private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 當前執行緒的前置節點的狀態!!!
    // 第waitStatus 初始化值為0,
    // 也因此當第1次進到這個方法時,會將前置結點的狀態置為 Node.SIGNAL。
    // 第 2次進來的時候,前置節點的waitStatus的狀態就為 Node.SIGNAL)。
    // 也就是說。aqs 只會讓你嘗試2次,都失敗後,就會被掛起
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don`t park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

// 執行緒被掛起呼叫該方法!!
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

讓我們總結一下,以及再回顧一下,為什麼aqs會被設計為雙向連結串列佇列。

aqs為了保證結點(即執行緒)的入佇列的安全。採用了CAS 以及死迴圈的方式(從程式碼中可看到,處處使用CAS)。
上面有說到,一個執行緒是否該被喚醒或者其他操作,只需要看前置結點的狀態即可。從shouldParkAfterFailedAcquire() 方法就可以看出這個設計。當前執行緒該做什麼操作,是看前置結點的狀態的。


③aqs如何釋放鎖

看程式碼前,先解釋一下,aqs是如何做的。aqs的做法就是,釋放當前鎖,然後喚醒頭結點的後繼結點,如果後繼結點為空,或者是被取消的,則從尾節點向前尋找一個未被取消的結點

public final boolean release(int arg) {
    // 嘗試釋放鎖
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 喚醒後繼結點
            unparkSuccessor(h);
        return true;
    }
    return false;
}

①ReentratLock 是如何實現鎖的釋放的

注:這裡看的是ReentrantLock的實現

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

解釋:設定 state 的狀態,如果 state == 0, 那麼說明鎖被釋放了。否則鎖還未被釋放(鎖重入!)


②aqs 如何喚醒其他結點

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
     // 清除狀態,還記得等待的執行緒會把前置節點的狀態置為 Node.SIGNAL(-1)嗎
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
     // 正常情況下,下一個結點就是被喚醒的節點。
     // 但是如果下一個結點為null, 或者是被取消的
     // 那麼從尾節點向前查詢一個未被取消的節點喚醒。
    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;
    }
    if (s != null)
        // 喚醒
        LockSupport.unpark(s.thread);
}

release的釋放比較簡單。還是可以看到,aqs被設計成雙向連結串列佇列的好處!!!

看原始碼,不能一下子就扎進去看,要先明白個大概,為什麼看原始碼?還不是為了學習作者是如何設計的。細節無論誰都記不清,最主要的是知道一個整體的流程,關鍵的程式碼!畢竟優秀的開源專案這麼多,難道每行程式碼都看??

相關文章