面試必問的AQS(AbstractQueuedSynchronizer),一文全搞定

前言

AQS是AbstractQueuedSynchronizer類的簡稱,雖然我們不會直接使用這個類,但是這個類是Java很多併發工具的底層實現。本文主要從源碼的角度,全方位的解析AQS類。

底層實現

首先看下哪些併發工具類是使用AQS實現的,使用IDEA就可以看到AQS
可以看到,CountDownLatchSemaphoreReentrantLock等等常見的工具類都是由AQS來實現的。所以不管是面試也好,還是自己研究底層實現也好,AQS類都是必須要重點關注的。

前置閱讀

對上述工具類不太瞭解的同學建議先看下以下幾篇

AQS

首先從AQS類的定義開始,逐步深入瞭解。AQS類的定義如下

/**
 * 可以看到AbstractQueuedSynchronizer是一個抽象類
 * 實現了Serializable 接口
 * @since 1.5
 * @author Doug Lea
 */
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    
   /**
    * The synchronization state.
    * state變量表示鎖的狀態
    * 0 表示未鎖定
    * 大於0表示已鎖定
    * 需要注意的是,這個值可以用來實現鎖的【可重入性】,例如 state=3 就表示鎖被同一個線程獲取了3次,想要完全解鎖,必須要對應的解鎖3次
    * 同時這個變量還是用volatile關鍵字修飾的,保證可見性
    */
    private volatile int state;
    
	/**
     * 等待隊列的頭節點,只能通過setHead方法修改
     * 如果head存在,能保證waitStatus狀態不爲CANCELLED
     */
    private transient volatile Node head;

    /**
     * 等待隊列的尾結點,只能通過enq方法來添加新的等待節點
     */
    private transient volatile Node tail;
}

AbstractQueuedSynchronizer從名字上就可看出本質是一個隊列(Queue),其內部維護着FIFO的雙向隊列,也就是CLH隊列。

CLH (Craig, Landin, and Hagersten) lock queue

這個隊列中的每一個元素都是一個Node,所以接下來了解一下其內部類Node,內部類Node的定義如下

static final class Node {

    // 節點正在共享模式下等待的標記
    static final Node SHARED = new Node();
    // 節點正在以獨佔模式等待的標記
    static final Node EXCLUSIVE = null;

    // waitStatus變量的可選值,因爲超時或者或者被中斷,節點會被設置成取消狀態。被取消的節點不會參與鎖競爭,狀態也不會再改變
    static final int CANCELLED =  1;
    // waitStatus變量的可選值,表示後繼節點處於等待狀態,如果當前節點釋放了鎖或者被取消,會通知後繼節點去運行
    static final int SIGNAL    = -1;
    // waitStatus變量的可選值,表示節點處於condition隊列中,正在等待被喚醒
    static final int CONDITION = -2;
    // waitStatus變量的可選值,下一次acquireShared應該無條件傳播
    static final int PROPAGATE = -3;

   // 節點的等待狀態
    volatile int waitStatus;

    // 前驅節點
    volatile Node prev;

    // 後繼節點
    volatile Node next;

    // 獲取同步狀態的線程
    volatile Thread thread;

    // 下一個condition隊列等待節點
    Node nextWaiter;

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

    // 返回前驅節點或者拋出異常
    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(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

有了前面的基礎,再來看下AQS的基本結構
AQS結構圖

核心方法

我們都知道CountDownLatchCyclicBarrierSemaphoreReentrantLock這些工具類中,有的只支持獨佔,如ReentrantLock#lock(),有的支持共享,多個線程同時執行,如Semaphore。並且,從前文Node類的定義也可以看到

// 節點正在共享模式下等待的標記
static final Node SHARED = new Node();
// 節點正在以獨佔模式等待的標記
static final Node EXCLUSIVE = null;

AQS實現了兩套加鎖解鎖的方式,那就是獨佔式共享式。我們先看下獨佔式的實現,獨佔式的實現,就從ReentrantLock#lock()方法開始。

ReentrantLock#lock

該方法定義如下

public void lock() {
    sync.lock();
}

其中syncAbstractQueuedSynchronizer的實現,我們知道,ReentrantLock支持公平鎖和非公平鎖,其實現類分別是FairSyncNonfairSync,我們看看公平鎖和非公平鎖分別是怎麼實現的

// FairSync 公平鎖的實現
final void lock() {
    acquire(1);
}

// NonfairSync 非公平鎖的實現
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}

可以看到,非公平鎖的實現僅僅是多了一個步驟:通過CAS的方式(compareAndSetState)嘗試改變state的狀態,修改成功後設置當前線程以獨佔的方式獲取了鎖,修改失敗執行的邏輯和公平鎖一樣。

這就是公平鎖和非公平鎖的本質區別

從這段代碼中可以看到,獨佔鎖加鎖的核心邏輯就是acquire方法,接下來就看看這個方法

acquire

該方法定義如下

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

該方法主要調用tryAcquire方法嘗試獲取鎖,成功返回true,失敗就將線程封裝成Node對象,放入隊列。

tryAcquire

tryAcquire方法在AQS中並沒有直接實現,而是採用模板方法的設計模式,交給子類去實現。我們來看公平鎖的實現。

protected final boolean tryAcquire(int acquires) {
	// 當前線程
    final Thread current = Thread.currentThread();
    // 獲取state狀態,0表示未鎖定,大於1表示重入
    int c = getState();
    if (c == 0) {
    	// 表示沒有線程獲取鎖
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            // 沒有比當前線程等待更久的線程了,通過CAS的方式修改state
            // 成功之後,設置當前擁有獨佔訪問權的線程
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
    	// 獨佔訪問權的線程就是當前線程,重入
    	// 此處就是【可重入性】的實現
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        // 直接修改state
        setState(nextc);
        return true;
    }
    return false;
}

可以看到該方法就是以獨佔的方式獲取鎖,獲取成功後返回true。從這個方法可以看出state變量是實現可重入性的關鍵。

非公平鎖的實現方式大同小異,感興趣的同學可以自行閱讀源碼。

acquire方法除了調用tryAcquire,還調用了acquireQueued(addWaiter(Node.EXCLUSIVE), arg),這裏分爲兩步,先看下addWaiter方法。

addWaiter

該方法用於把當前線程封裝成一個Node節點,並加入隊列。方法定義如下

/**
 * Creates and enqueues node for current thread and given mode.
 * 爲當前線程和給定模式創建並排隊節點,給的的模式分爲:
 * 1、Node.EXCLUSIVE:獨佔模式
 * 2、Node.SHARED:共享模式
 * 
 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
 */
private Node addWaiter(Node mode) {
	// 創建Node節點
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    // 嘗試快速添加尾結點,失敗就執行enq方法
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        // CAS的方式設置尾結點
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 快速添加失敗,執行該方法
    enq(node);
    return node;
}

enq方法定義如下

/**
 * Inserts node into queue, initializing if necessary. See picture above.
 * 將節點插入隊列,必要時進行初始化
 * 
 * @param node the node to insert
 * @return node's predecessor 
 */
private Node enq(final Node node) {
    for (;;) {
    	// 自旋
        Node t = tail;
        if (t == null) { // Must initialize
        	// 尾結點爲空,隊列還沒有進行初始化
        	// 設置頭節點
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            // CAS的方式設置尾結點,失敗就進入下次循環
            // 也就是【自旋 + CAS】的方式保證設置成功
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

可以看到該方法就是用來往隊列尾部插入一個新的節點,通過自旋 + CAS的方式保證線程安全插入成功

需要注意的是,該方法返回的Node節點不是新插入的節點,而是新插入節點的前驅節點。

acquireQueued

該方法定義如下

/**
 * Acquires in exclusive uninterruptible mode for thread already in
 * queue. Used by condition wait methods as well as acquire.
 *
 */
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)) {
            	// 前驅節點是頭節點,並且已經獲取了鎖(tryAcquire方法在前文中詳細講解過)
            	// 就把當前節點設置成頭節點(因爲前驅節點已經獲取了鎖,所以前驅節點不用再留在隊列)
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 如果前驅節點不是頭節點或者沒有獲取鎖
                // shouldParkAfterFailedAcquire方法用於判斷當前線程是否需要被阻塞
                // parkAndCheckInterrupt方法用於阻塞線程並且檢測線程是否被中斷
                // 沒搶到鎖的線程需要被阻塞,避免一直去爭搶鎖,浪費CPU資源
                interrupted = true;
        }
    } finally {
        if (failed)
        	// 自旋異常退出,取消正在進行鎖爭搶
            cancelAcquire(node);
    }
}

shouldParkAfterFailedAcquire

shouldParkAfterFailedAcquire方法定義如下,用於判斷當前線程是否需要被阻塞

/**
 * Checks and updates status for a node that failed to acquire.
 * Returns true if thread should block. This is the main signal
 * control in all acquire loops.  Requires that pred == node.prev.
 *
 * @param pred node's predecessor holding status
 * @param node the node
 * @return {@code true} if thread should block
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	// 獲取前驅節點的等待狀態
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * SIGNAL表示後繼節點處於等待狀態,如果當前節點釋放了鎖或者被取消,會通知後繼節點去運行
         * 所以作爲後繼節點,node直接返回true,表示需要被阻塞
         */
        return true;
    if (ws > 0) {
        /*
         * 前驅節點被取消了,需要從隊列中移除,並且循環找到下一個不是取消狀態的節點
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * 通過CAS將前驅節點的status設置成SIGNAL
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

parkAndCheckInterrupt

parkAndCheckInterrupt方法定義如下,用於阻塞線程並且檢測線程是否被中斷

private final boolean parkAndCheckInterrupt() {
	// 阻塞當前線程
    LockSupport.park(this);
    // 檢測當前線程是否被中斷(該方法會清除中斷標識位)
    return Thread.interrupted();
}

至此,獨佔鎖的整個加鎖過程就已經完成。再來回顧下整個流程

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

首先執行tryAcquire方法用於嘗試獲取鎖,成功後就直接返回,失敗後就通過addWaiter方法把當前線程封裝成一個Node,加到隊列的尾部,再通過acquireQueued方法嘗試獲取同步鎖,成功獲取鎖的線程的Node節點會被移出隊列。

如果以上條件都滿足,會執行selfInterrupt方法中斷當前線程。

看完了獨佔鎖的加鎖,再來看看獨佔鎖的解鎖。同樣從ReentrantLock入手

ReentrantLock#unlock

方法定義如下

public void unlock() {
    sync.release(1);
}

我們已經知道了sync是AQS的實現,所以直接查看AQS中的release方法

/**
 * Releases in exclusive mode.  Implemented by unblocking one or
 * more threads if {@link #tryRelease} returns true.
 * This method can be used to implement method {@link Lock#unlock}.
 *
 * @param arg the release argument.  This value is conveyed to
 *        {@link #tryRelease} but is otherwise uninterpreted and
 *        can represent anything you like.
 * @return the value returned from {@link #tryRelease}
 */
public final boolean release(int arg) {
    if (tryRelease(arg)) {
    	// 嘗試釋放鎖
        Node h = head;
        if (h != null && h.waitStatus != 0)
        	// 頭節點已經釋放,喚醒後繼節點
            unparkSuccessor(h);
        return true;
    }
    return false;
}

相信大家已經猜到了,和加鎖時一樣,這裏的tryRelease方法同樣使用了模板方法的設計模式,其真正的邏輯由子類實現

tryRelease

方法定義如下

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;
        // 設置獨佔鎖的持有者爲null
        setExclusiveOwnerThread(null);
    }
    // 設置AQS的state
    setState(c);
    return free;
}

unparkSuccessor

unparkSuccessor方法用於喚醒後繼節點,其定義如下

/**
 * Wakes up node's successor, if one exists.
 *
 * @param node the node
 */
private void unparkSuccessor(Node node) {
   // 獲取當前節點的狀態
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
    	// 當前節點的後繼節點爲null,或者被取消了
        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);
}

前文說過AQS實現了兩套同步邏輯,也就是獨佔式共享式。看完了獨佔式鎖的實現,再來看一下共享式。這裏以Semaphore爲例。

Semaphore#acquire

該方法是作用是請求一個許可,如果暫時沒有可用的許可,則被阻塞,等待將來的某個時間被喚醒。因爲Semaphore可以允許多個線程同時執行,所以可以看成是共享鎖的實現。該方法定義如下

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

sync是AQS的實現,可以看到acquire方法底層調用的是acquireSharedInterruptibly方法。

在JDK中,與鎖相關的方法,Interruptibly表示可中斷,也就是可中斷鎖。可中斷鎖的意思是線程在等待獲取鎖的過程中可以被中斷,換言之,線程在等待鎖的過程中可以響應中斷

接下來看看acquireSharedInterruptibly方法的實現

acquireSharedInterruptibly

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
    	// 檢測線程的中斷中斷狀態,如果已經被中斷了,就響應中斷
    	// 該方法會清除線程的中斷標識位
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

tryAcquireShared

tryAcquireShared方法,相信大家已經能看出來,這裏使用了模板方法模式,具體實現由子類去實現。Semaphore也實現了公平模式和非公平模式。公平的方式和非公平的方式實現邏輯大同小異。所以具體看下公平模式下的實現方式

protected int tryAcquireShared(int acquires) {
    for (;;) {
    	// 自旋
        if (hasQueuedPredecessors())
        	// 如果有線程排在自己的前面(公平鎖排隊),直接返回
            return -1;
        // 獲取同步狀態的值
        int available = getState();
        // 可用的(許可)減去申請的,等於剩餘的
        int remaining = available - acquires;
        if (remaining < 0 ||
            compareAndSetState(available, remaining))
            // 如果剩餘的小於0,或者設置狀態成功,就返回,如果設置失敗,則進入下一次循環
            // 如果剩餘小於0,返回負數,表示失敗
            // 如果設置狀態成功,表示申請許可成功,返回正數
            return remaining;
    }
}

此處還是自旋 + CAS的方式保證線程安全和設置成功。

doAcquireSharedInterruptibly

doAcquireSharedInterruptibly方法定義如下

/**
 * Acquires in shared interruptible mode.
 * 在共享可中斷模式下請求(許可)
 */
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 爲當前線程和給定模式創建節點並插入隊列尾部,addWaiter方法前文講解過
    final Node node = addWaiter(Node.SHARED);
    // 操作是否失敗
    boolean failed = true;
    try {
        for (;;) {
        	// 自旋
        	// 獲取當前節點的前驅節點
            final Node p = node.predecessor();
            if (p == head) {
            	// 如果前驅節點是頭節點,以共享的方式請求獲取鎖,tryAcquireShared方法前文講解過
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                	// 成功獲取鎖,設置頭節點和共享模式傳播
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                // 如果前驅節點不是頭節點或者沒有獲取鎖
                // shouldParkAfterFailedAcquire方法用於判斷當前線程是否需要被阻塞,該方法前文講解過
                // parkAndCheckInterrupt方法用於阻塞線程並且檢測線程是否被中斷,該方法前文講解過
                // 沒搶到鎖的線程需要被阻塞,避免一直去爭搶鎖,浪費CPU資源
                throw new InterruptedException();
        }
    } finally {
        if (failed)
        	// 自旋異常退出,取消正在進行鎖爭搶
            cancelAcquire(node);
    }
}

加鎖的邏輯已經完成,再來看看解鎖的邏輯。

Semaphore#release

release用於釋放許可,其方法定義如下

public void release() {
    sync.releaseShared(1);
}

releaseShared

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

tryReleaseShared

protected final boolean tryReleaseShared(int releases) {
    for (;;) {
    	// 自旋
    	// 獲取同步狀態的值
        int current = getState();
        // 可用的(許可)加上釋放的,等於剩餘的
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        if (compareAndSetState(current, next))
        	// CAS的方式設置同步狀態
            return true;
    }
}

可以看到此處依舊是自旋 + CAS的操作

doReleaseShared

/**
 * Release action for shared mode -- signals successor and ensures
 * propagation. (Note: For exclusive mode, release just amounts
 * to calling unparkSuccessor of head if it needs signal.)
 */
private void doReleaseShared() {
    /*
     * Ensure that a release propagates, even if there are other
     * in-progress acquires/releases.  This proceeds in the usual
     * way of trying to unparkSuccessor of head if it needs
     * signal. But if it does not, status is set to PROPAGATE to
     * ensure that upon release, propagation continues.
     * Additionally, we must loop in case a new node is added
     * while we are doing this. Also, unlike other uses of
     * unparkSuccessor, we need to know if CAS to reset status
     * fails, if so rechecking.
     */
    for (;;) {
    	// 自旋
    	// 記錄頭節點
        Node h = head;
        if (h != null && h != tail) {
        	// 頭節點不爲null,且不等於尾結點,說明隊列中還有節點
        	// 獲取頭節點等待狀態
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
            	// 頭節點等待狀態是SIGNAL
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                	// 如果修改節點等待狀態失敗,進入下一次循環
                    continue;            // loop to recheck cases
                // 修改成功後,喚醒後繼節點,unparkSuccessor前文講過
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

總結

AQS可以說是整個併發編程中最難的一個類。但是理解AQS的實現卻非常重要,因爲它是JDK中和其他同步工具實現的基礎。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章