【細談Java并發】談談AQS

1、概述

談到并發,不得不談ReentrantLock;而談到ReentrantLock,不得不談AbstractQueuedSynchronizer(AQS)!

AQS定義了一個抽象的隊列來進行同步操作,很多同步類都依賴于它,例如常用的ReentrantLock/Semaphore/CountDownLatch等。下面我們來通過源碼分析解開AQS的神秘面紗。

2、框架

image

它維護了一個volatile int state(代表共享資源)和一個FIFO線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)。這里volatile是核心關鍵詞,具體volatile的語義,在此不述。state的訪問方式有三種:

  • getState()
  • setState()
  • compareAndSetState()

AQS定義兩種資源共享方式:Exclusive(獨占,只有一個線程能執行,如ReentrantLock)和Share(共享,多個線程可同時執行,如Semaphore/CountDownLatch)。

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

/**
 * 獨占式獲取同步狀態,實現該方法需要查詢當前狀態并判斷同步狀態是否符合預期,然后再進行CAS設置同步狀態。
 * 成功返回true,失敗返回false。
 */
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 獨占式釋放同步狀態,等待獲取同步狀態的線程將有機會獲取同步狀態。
 * 成功返回true,失敗返回false。
 */
protected boolean tryRelease(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 共享式獲取同步狀態。
 * 1. 返回負數表示失敗。
 * 2. 0表示成功,但沒有剩余可用資源。
 * 3. 正數表示成功,且有剩余資源。
 */
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 共享式釋放同步狀態。
 * 如果釋放后允許喚醒后續等待結點返回true,否則返回false。
 */
protected boolean tryReleaseShared(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 當前同步器是否在獨占模式下被線程占用,一般該方法表示是否被當前線程所獨占。
 * 只有用到condition才需要去實現它。
 */
protected boolean isHeldExclusively() {
    throw new UnsupportedOperationException();
}

以ReentrantLock為例,state初始化為0,表示未鎖定狀態。A線程lock()時,會調用tryAcquire()獨占該鎖并將state+1。此后,其他線程再tryAcquire()時就會失敗,直到A線程unlock()到state=0(即釋放鎖)為止,其它線程才有機會獲取該鎖。當然,釋放鎖之前,A線程自己是可以重復獲取此鎖的(state會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多么次,這樣才能保證state是能回到零態的。

再以CountDownLatch以例,任務分為N個子線程去執行,state也初始化為N(注意N要與線程個數一致)。這N個子線程是并行執行的,每個子線程執行完后countDown()一次,state會CAS減1。等到所有子線程都執行完后(即state=0),會unpark()主調用線程,然后主調用線程就會從await()函數返回,繼續后余動作。

一般來說,自定義同步器要么是獨占方法,要么是共享方式,但AQS也支持自定義同步器同時實現獨占和共享兩種方式,如ReentrantReadWriteLock。

3、源碼分析

我們先來看看Node元素的類結構圖:

static final class Node {
    static final Node SHARED = new Node();
    static final Node EXCLUSIVE = null;
    //表示當前的線程被取消;
    static final int CANCELLED =  1;
    //表示當前節點的后繼節點包含的線程需要運行,也就是unpark;
    static final int SIGNAL    = -1;
    //表示當前節點在等待condition,也就是在condition隊列中;
    static final int CONDITION = -2;
    //表示當前場景下后續的acquireShared能夠得以執行;
    static final int PROPAGATE = -3;
    //表示節點的狀態。默認為0,表示當前節點在sync隊列中,等待著獲取鎖。
    //其它幾個狀態為:CANCELLED、SIGNAL、CONDITION、PROPAGATE
    volatile int waitStatus;
    //前驅節點
    volatile Node prev;
    //后繼節點
    volatile Node next;
    //獲取鎖的線程
    volatile Thread thread;
    //存儲condition隊列中的后繼節點。
    Node nextWaiter;
    ......
}

3.1、acquire(int)

/**
 * 獨占獲取同步狀態,如果當前線程獲取同步狀態成功,則由該方法返回,否則,
 * 將會進入同步隊列等待,該方法會調用重寫的tryAcquire(int arg)方法
 */
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

該方法是獨占模式下線程獲取共享資源的頂層入口。如果獲取到資源,線程直接返回,否則進入等待隊列,直到獲取到資源為止,且整個過程忽略中斷的影響。ReentrantLock的lock方法就是調用的該方法來獲取鎖。

方法的執行流程如下:

  1. 調用自定義同步器的tryAcquire()嘗試直接去獲取資源,如果成功則直接返回。
  2. 沒成功,則addWaiter()將該線程加入等待隊列的尾部,并標記為獨占模式。
  3. acquireQueued()使線程在等待隊列中休息,有機會時(輪到自己,會被unpark())會去嘗試獲取資源。獲取到資源后才返回。如果在整個等待過程中被中斷過,則返回true,否則返回false。
  4. 如果線程在等待過程中被中斷過,它是不響應的。只是獲取資源后才再進行自我中斷selfInterrupt(),將中斷補上。

可能單看這個流程還是看不太明白,沒關系,接下來我們會一一擊破,等會回過頭來再看這個流程就會非常清晰了。

3.1.1、tryAcquire(int)

上面我們也說過這個方法(具體的資源的獲取/釋放)是需要實現類進行重寫的。至于能不能重入,能不能加鎖,那就看具體的自定義同步器怎么去設計了。當然,自定義同步器在進行資源訪問時要考慮線程安全的影響。

下面有的方法比較簡單,直接看注釋吧。

3.1.2、addWaiter(Node)

/**
 * 將當前線程加入到等待隊列的隊尾,并返回當前線程所在的結點
 */
private Node addWaiter(Node mode) {
    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;
        }
    }
    // 如果首節點為空或者cas添加失敗,則進入enq方法通過自旋方式入隊列(保證成功)
    enq(node);
    return node;
}

3.1.3、enq(Node)

/**
 * 將node加入隊尾
 */
private Node enq(final Node node) {
    // 自旋重試
    for (;;) {
        Node t = tail;
        // 當前沒有節點,構造一個new Node(),將head和tail指向它
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 當前有節點,將傳入的Node放在鏈表的最后
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

這里用到了CAS自旋來把Node放到隊尾。

3.1.4、acquireQueued(Node, int)

OK,通過tryAcquire()和addWaiter(),該線程獲取資源失敗,已經被放入等待隊列尾部了。聰明的你立刻應該能想到該線程下一部該干什么了吧:進入等待狀態休息,直到其他線程徹底釋放資源后喚醒自己,自己再拿到資源,然后就可以去干自己想干的事了。沒錯,就是這樣!是不是跟醫院排隊拿號有點相似~~acquireQueued()就是干這件事:在等待隊列中排隊拿號(中間沒其它事干可以休息),直到拿到號后再返回。這個函數非常關鍵,還是上源碼吧

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true; // 標記是否成功拿到資源
    try {
        boolean interrupted = false; // 標記等待過程中是否被中斷過
        for (;;) {
            final Node p = node.predecessor();  // node的前一個節點
            // 如果前一個節點是head,說明當前node節點是第二個節點,接著嘗試去獲取資源
            // Note:可能是head釋放完資源喚醒自己的,當然也可能被interrupt了
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted; // 返回等待過程中是否被中斷過
            }
            
            // 如果自己可以休息了,就進入waiting狀態,直到被unpark()
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                interrupted = true; // 如果等待過程中被中斷過,哪怕只有那么一次,就將interrupted標記為true
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

/**
 * 此方法主要用于檢查狀態,看看自己是否真的可以去休息了
 * 1.如果pred的waitStatus是SIGNAL,直接返回true
 * 2.如果pred的waitStatus>0,也就是CANCELLED,向前一直找到<=0的節點,讓節點的next指向node
 * 3.如果pred的waitStatus<=0,改成SIGNAL
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        // 如果已經告訴前驅拿完號后通知自己一下,那就可以安心休息了
        return true;
    if (ws > 0) {
        /*
         * 如果前節點放棄了,那就一直往前找,直到找到最近一個正常等待的狀態,并排在它的后邊。
         * 注意:那些放棄的結點,由于被自己“加塞”到它們前邊,它們相當于形成一個無引用鏈,
         * 稍后就會被保安大叔趕走了(GC回收)!
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 如果前節點正常,那就把前節點的狀態設置成SIGNAL,告訴它拿完號后通知自己一下。有可能失敗,人家說不定剛剛釋放完呢!
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

/**
 * 讓線程去休息,真正進入等待狀態
 */
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 調用park()使線程進入waiting狀態
    return Thread.interrupted(); // 如果被喚醒,查看是否被中斷(該方法會重置標識位)
}

上面的parkAndCheckInterrupt方法才是真正讓線程“等待”的方法,其中用到了LockSupport的park方法,關于LockSupport可以參考我之前的博文:Java并發之LockSupport

總結一下,acquireQueued總共做了3件事:

  1. 結點進入隊尾后,檢查狀態,找到安全休息點。
  2. 調用park()進入waiting狀態,等待unpark()或interrupt()喚醒自己。
  3. 被喚醒后,看自己是不是有資格能拿到號。如果拿到,head指向當前結點,并返回從入隊到拿到號的整個過程中是否被中斷過;如果沒拿到,繼續流程1。

3.1.5、acquire總結

我們回過頭來再看acquire方法,發現還有一個方法沒有說到,那就是selfInterrupt方法,在

// 重新設置中斷標識位
static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

由于此函數是重中之重,最后再用流程圖總結一下:

acquire

3.2、release(int)

上一小節已經把acquire()說完了,這一小節就來講講它的反操作release()吧。此方法是獨占模式下線程釋放資源的頂層入口。它會釋放指定量的資源,如果徹底釋放了(即state=0),它會喚醒等待隊列里的其他線程來獲取資源。這也正是unlock()的語義,當然不僅僅只限于unlock()。下面是release()的源碼:

/**
 * 釋放資源
 */
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()的返回值來判斷該線程是否已經完成釋放掉資源了!所以自定義同步器在設計tryRelease()的時候要明確這一點!!

3.2.1、tryRelease(int)

跟tryAcquire()一樣,這個方法是需要獨占模式的自定義同步器去實現的。正常來說,tryRelease()都會成功的,因為這是獨占模式,該線程來釋放資源,那么它肯定已經拿到獨占資源了,直接減掉相應量的資源即可(state-=arg),也不需要考慮線程安全的問題。但要注意它的返回值,上面已經提到了,release()是根據tryRelease()的返回值來判斷該線程是否已經完成釋放掉資源了!所以自義定同步器在實現時,如果已經徹底釋放資源(state=0),要返回true,否則返回false。

3.2.2、unparkSuccessor(Node)

此方法用于喚醒等待隊列中下一個線程。下面是源碼:

private void unparkSuccessor(Node node) {
    // 這里,node一般為當前線程所在的結點。
    int ws = node.waitStatus;
    if (ws < 0) // 置零當前線程所在的結點狀態,允許失敗。
        compareAndSetWaitStatus(node, ws, 0);

    // 找到下一個需要喚醒的結點s
    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);   // 喚醒
}

3.2.3、release總結

release()是獨占模式下線程釋放共享資源的頂層入口。它會釋放指定量的資源,如果徹底釋放了(即state=0),它會喚醒等待隊列里的其他線程來獲取資源。

3.3、acquireShared(int)

此方法是共享模式下線程獲取共享資源的頂層入口。它會獲取指定量的資源,獲取成功則直接返回,獲取失敗則進入等待隊列,直到獲取到資源為止,整個過程忽略中斷。下面是acquireShared()的源碼:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

這里tryAcquireShared()依然需要自定義同步器去實現。但是AQS已經把其返回值的語義定義好了:負值代表獲取失敗;0代表獲取成功,但沒有剩余資源;正數表示獲取成功,還有剩余資源,其他線程還可以去獲取。

3.3.1、doAcquireShared(int)

此方法用于將當前線程加入等待隊列尾部休息,直到其他線程釋放資源喚醒自己,自己成功拿到相應量的資源后才返回。

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);   //加入隊列尾部
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);  // 嘗試獲取資源
                if (r >= 0) {
                    setHeadAndPropagate(node, r);   // 將head指向自己,還有剩余資源可以再喚醒之后的線程
                    p.next = null; // help GC
                    if (interrupted)    // 如果等待過程中被打斷過,此時將中斷補上
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            // 判斷狀態,尋找安全點,進入waiting狀態,等著被unpark()或interrupt()
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

有木有覺得跟acquireQueued()很相似?對,其實流程并沒有太大區別。只不過這里將補中斷的selfInterrupt()放到doAcquireShared()里了,而獨占模式是放到acquireQueued()之外,其實都一樣,不知道Doug Lea是怎么想的。

除此之外,有一個方法很重要:setHeadAndPropagate。它除了重新標記head指向的節點外,還有一個重要的作用,那就是propagate(傳遞),也就是共享的意思。

用圖舉個例子:


image

因為線程B的讀鎖無法直接獲得鎖,所以需要在Sync隊列中等待,導致后面其他線程的讀鎖都得等待。

當線程A的讀鎖釋放后,線程B的寫鎖獲得鎖,當它釋放后,線程B的讀鎖會獲取到鎖,并傳遞給后面的節點,傳遞的事情就是在setHeadAndPropagate里做的,我們來看看它是如何做的。

3.3.2、setHeadAndPropagate(Node, int)

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head;//記錄當前頭節點
    //設置新的頭節點,即把當前獲取到鎖的節點設置為頭節點
    //注:這里是獲取到鎖之后的操作,不需要并發控制
    setHead(node);
    //這里意思有兩種情況是需要執行喚醒操作
    //1.propagate > 0 表示調用方指明了后繼節點需要被喚醒
    //2.頭節點后面的節點需要被喚醒(waitStatus<0),不論是老的頭結點還是新的頭結點
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        //如果當前節點的后繼節點是共享類型或者沒有后繼節點,則進行喚醒
        //這里可以理解為除非明確指明不需要喚醒(后繼等待節點是獨占類型),否則都要喚醒
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

此方法在setHead()的基礎上多了一步,就是自己蘇醒的同時,如果條件符合(比如還有剩余資源),還會去喚醒后繼結點,畢竟是共享模式!這樣,形成了一個喚醒鏈,直到寫鎖的節點就停止。

doReleaseShared()我們留著下一小節的releaseShared()里來講。

3.3.3、acquireShared總結

OK,至此,acquireShared()也要告一段落了。讓我們再梳理一下它的流程:

  1. tryAcquireShared()嘗試獲取資源,成功則直接返回;

其實跟acquire()的流程大同小異,只不過多了個自己拿到資源后,還會去喚醒后繼隊友的操作(這才是共享嘛)

3.4、releaseShared()

上一小節已經把acquireShared()說完了,這一小節就來講講它的反操作releaseShared()吧。此方法是共享模式下線程釋放共享資源的頂層入口。它會釋放指定量的資源,如果成功釋放且允許喚醒等待線程,它會喚醒等待隊列里的其他線程來獲取資源。下面是releaseShared()的源碼:

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {// 嘗試釋放資源
        doReleaseShared();// 喚醒后繼結點
        return true;
    }
    return false;
}

此方法的流程也比較簡單,一句話:釋放掉資源后,喚醒后繼。跟獨占模式下的release()相似,但有一點稍微需要注意:獨占模式下的tryRelease()在完全釋放掉資源(state=0)后,才會返回true去喚醒其他線程,這主要是基于獨占下可重入的考量;而共享模式下的releaseShared()則沒有這種要求,共享模式實質就是控制一定量的線程并發執行,那么擁有資源的線程在釋放掉部分資源時就可以喚醒后繼等待結點。例如,資源總量是13,A(5)和B(7)分別獲取到資源并發運行,C(4)來時只剩1個資源就需要等待。A在運行過程中釋放掉2個資源量,然后tryReleaseShared(2)返回true喚醒C,C一看只有3個仍不夠繼續等待;隨后B又釋放2個,tryReleaseShared(2)返回true喚醒C,C一看有5個夠自己用了,然后C就可以跟A和B一起運行。而ReentrantReadWriteLock讀鎖的tryReleaseShared()只有在完全釋放掉資源(state=0)才返回true,所以自定義同步器可以根據需要決定tryReleaseShared()的返回值。

3.4.1、doReleaseShared()

此方法主要用于喚醒后繼。下面是它的源碼:

private void doReleaseShared() {
    /*
     * 如果head需要通知下一個節點,調用unparkSuccessor
     * 如果不需要通知,需要在釋放后把waitStatus改為PROPAGATE來繼續傳播
     * 此外,我們必須通過自旋來CAS以防止操作時有新節點加入
     * 另外,不同于其他unparkSuccessor的用途,我們需要知道CAS設置狀態失敗的情況,
     * 以便進行重新檢查。
     */
    for (;;) {
        //喚醒操作由頭結點開始,注意這里的頭節點已經是上面新設置的頭結點了
        //其實就是喚醒上面新獲取到共享鎖的節點的后繼節點
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            //表示后繼節點需要被喚醒
            if (ws == Node.SIGNAL) {
                //這里需要控制并發,因為入口有setHeadAndPropagate跟release兩個,避免兩次unpark
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;           
                //執行喚醒操作
                unparkSuccessor(h);
            }
            //如果后繼節點暫時不需要喚醒,則把當前節點狀態設置為PROPAGATE確保以后可以傳遞下去
            else if (ws == 0 &&
                    !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;               
        }
        //如果頭結點沒有發生變化,表示設置完成,退出循環
        //如果頭結點發生變化,比如說其他線程獲取到了鎖,為了使自己的喚醒動作可以傳遞,必須進行重試
        if (h == head)                   
            break;
    }
}

3.5、Condition

在沒有Lock之前,我們使用synchronized來控制同步,配合Object的wait()、notify()系列方法可以實現等待/通知模式。在Java SE5后,Java提供了Lock接口,相對于Synchronized而言,Lock提供了條件Condition,對線程的等待、喚醒操作更加詳細和靈活。

Condition提供了一系列的方法來對阻塞和喚醒線程:

public interface Condition {

    /**
     * 造成當前線程在接到信號或被中斷之前一直處于等待狀態。
     */
    void await() throws InterruptedException;

    /**
     * 造成當前線程在接到信號之前一直處于等待狀態。【注意:該方法對中斷不敏感】。
     */
    void awaitUninterruptibly();

    /**
     * 造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處于等待狀態。
     * 返回值表示剩余時間,如果在nanosTimesout之前喚醒,那么返回值 = nanosTimeout - 消耗時間,
     * 如果返回值 <= 0 ,則可以認定它已經超時了。
     */
    long awaitNanos(long nanosTimeout) throws InterruptedException;

    /**
     * 造成當前線程在接到信號、被中斷或到達指定等待時間之前一直處于等待狀態。
     */
    boolean await(long time, TimeUnit unit) throws InterruptedException;

    /**
     * 造成當前線程在接到信號、被中斷或到達指定最后期限之前一直處于等待狀態。
     * 如果沒有到指定時間就被通知,則返回true,否則表示到了指定時間,返回返回false。
     */
    boolean awaitUntil(Date deadline) throws InterruptedException;

    /**
     * 喚醒一個等待線程。該線程從等待方法返回前必須獲得與Condition相關的鎖。
     */
    void signal();

    /**
     * 喚醒所有等待線程。能夠從等待方法返回的線程必須獲得與Condition相關的鎖。
     */
    void signalAll();
}

Condition是一種廣義上的條件隊列。他為線程提供了一種更為靈活的等待/通知模式,線程在調用await方法后執行掛起操作,直到線程等待的某個條件為真時才會被喚醒。Condition必須要配合鎖一起使用,因為對共享狀態變量的訪問發生在多線程環境下。一個Condition的實例必須與一個Lock綁定,因此Condition一般都是作為Lock的內部實現。

我們這里要說的Condition其實就是AQS的一個內部類:ConditionObject

public class ConditionObject implements Condition, java.io.Serializable {
    //頭節點
    private transient Node firstWaiter;
    //尾節點
    private transient Node lastWaiter;
}

每個Condition對象都包含著一個FIFO隊列,該隊列是Condition對象通知/等待功能的關鍵。在隊列中每一個節點都包含著一個線程引用,該線程就是在該Condition對象上等待的線程。Condition擁有首節點(firstWaiter),尾節點(lastWaiter)。當前線程調用await()方法,將會以當前線程構造成一個節點(Node),并將節點加入到該隊列的尾部。

image

Condition的隊列結構比CLH同步隊列的結構簡單些,新增過程較為簡單只需要將原尾節點的nextWaiter指向新增節點,然后更新lastWaiter即可。

因為大多數方法都差不多,我們這里只重點講解await和signal方法。我們來看看源碼是如何實現的。

3.5.1、await()

調用Condition的await()方法會使當前線程進入等待狀態,同時會加入到Condition等待隊列同時釋放鎖。當從await()方法返回時,當前線程一定是獲取了Condition相關連的鎖。

public final void await() throws InterruptedException {
    // 當前線程中斷
    if (Thread.interrupted())
        throw new InterruptedException();
    //當前線程加入等待隊列
    Node node = addConditionWaiter();
    //釋放鎖,返回釋放之前的狀態
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    /**
     * 檢測此節點的線程是否在Sync隊列里,如果不在,則說明該線程還不具備競爭鎖的資格,則繼續等待
     * 直到檢測到此節點在Sync隊列里
     */
    while (!isOnSyncQueue(node)) {
        //線程掛起
        LockSupport.park(this);
        //如果已經中斷了,則退出循環
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    //競爭同步狀態
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    //清理下條件隊列中的不是在等待條件的節點
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0) // 對中斷狀態進行判斷,是需要拋異常還是重置中斷位
        reportInterruptAfterWait(interruptMode);
}

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 檢查隊尾的節點的狀態,清理掉CANCELLED的節點。
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter; // 重新獲取lastWaiter
    }
    // 新建一個CONDITION節點放到隊尾
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

/**
 * 釋放當前狀態值,返回已保存的狀態
 */
final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        int savedState = getState();
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

/**
 * 是不是在Sync隊列里
 */
final boolean isOnSyncQueue(Node node) {
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    if (node.next != null) // 這種情況node一定在Sync隊列中
        return true;
    // 從隊尾往前找node,找到返回true,否則返回false(這種情況很少發生)
    return findNodeFromTail(node);
}

總結一下,await其實就做了幾件事:

  1. 將當前線程的Condition節點放入等待隊列中。
  2. 釋放Sync中的鎖。
  3. 確認節點不在Sync中了,調用park掛起線程。等待signal喚醒。
  4. 喚醒后競爭鎖。

注:await不會消化中斷異常,如果在await前就interrupt了,在await第一行就會拋出異常。而在await中進行interrupt的話,也會拋出異常。

3.5.2、signal()

調用Condition的signal()方法,將會喚醒在等待隊列中等待最長時間的節點(條件隊列里的首節點),在喚醒節點前,會將節點移到CLH同步隊列中。

public final void signal() {
    //檢測當前線程是否為擁有獨占鎖
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //頭節點,喚醒條件隊列中的第一個節點
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

private void doSignal(Node first) {
    do {
        //修改頭結點,完成舊頭結點的移出工作
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&   //將節點移動到CLH同步隊列中
            (first = firstWaiter) != null);
}

final boolean transferForSignal(Node node) {
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;   // CAS失敗,說明node已經被CANCELLED了

    //將節點加入到syn隊列中去,返回的是syn隊列中node節點前面的一個節點
    Node p = enq(node);
    int ws = p.waitStatus;
    //如果結點p的狀態為cancel 或者修改waitStatus失敗,則直接喚醒
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

可以發現,signal主要是拿到Condition隊列里的第一個節點并調用doSignal方法,主要做了以下幾件事:

  1. 修改節點waitStatus(把要喚醒的節點waitStatus從CONDITION改為0)。
  2. 把節點放入syn隊列中(這樣該節點就可以競爭鎖了)。
  3. 如果節點在Sync隊列的前一個節點狀態是CANCELLED,或者把狀態改為SIGNAL時失敗了,立即喚醒當前節點競爭鎖(如果不這么做,這么節點將永遠不會被喚醒去競爭鎖,導致一直等待)。

signalAll其實就是多次signal。它從Condition隊列的首節點一直遍歷到最后去調用transferForSignal,這里就不講了。

3.6、小結

本節我們詳解了獨占和共享兩種模式下獲取-釋放資源(acquire-release、acquireShared-releaseShared)以及Condition的源碼,相信大家都有一定認識了。

值得注意的是,acquire()和acquireSahred()兩種方法下,線程在等待隊列中都是忽略中斷的。AQS也支持響應中斷的,acquireInterruptibly()/acquireSharedInterruptibly()即是,這里相應的源碼跟acquire()和acquireSahred()差不多,這里就不再詳解了。

4、Mutex(互斥鎖)

Mutex是一個不可重入的互斥鎖實現。鎖資源(AQS里的state)只有兩種狀態:0表示未鎖定,1表示鎖定。下邊是Mutex的核心源碼:

class Mutex implements Lock, java.io.Serializable {
    // 自定義同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 判斷是否鎖定狀態
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // 嘗試獲取資源,立即返回。成功則返回true,否則false。
        public boolean tryAcquire(int acquires) {
            assert acquires == 1; // 這里限定只能為1個量
            if (compareAndSetState(0, 1)) {//state為0才設置為1,不可重入!
                setExclusiveOwnerThread(Thread.currentThread());//設置為當前線程獨占資源
                return true;
            }
            return false;
        }

        // 嘗試釋放資源,立即返回。成功則為true,否則false。
        protected boolean tryRelease(int releases) {
            assert releases == 1; // 限定為1個量
            if (getState() == 0)//既然來釋放,那肯定就是已占有狀態了。只是為了保險,多層判斷!
                throw new IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);//釋放資源,放棄占有狀態
            return true;
        }
    }

    // 真正同步類的實現都依賴繼承于AQS的自定義同步器!
    private final Sync sync = new Sync();

    //lock<-->acquire。兩者語義一樣:獲取資源,即便等待,直到成功才返回。
    public void lock() {
        sync.acquire(1);
    }

    //tryLock<-->tryAcquire。兩者語義一樣:嘗試獲取資源,要求立即返回。成功則為true,失敗則為false。
    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    //unlock<-->release。兩者語文一樣:釋放資源。
    public void unlock() {
        sync.release(1);
    }

    //鎖是否占有狀態
    public boolean isLocked() {
        return sync.isHeldExclusively();
    }
}

同步類在實現時一般都將自定義同步器(sync)定義為內部類,供自己使用;而同步類自己(Mutex)則實現某個接口,對外服務。當然,接口的實現要直接依賴sync,它們在語義上也存在某種對應關系!!而sync只用實現資源state的獲取-釋放方式tryAcquire-tryRelelase,至于線程的排隊、等待、喚醒等,上層的AQS都已經實現好了,我們不用關心。

除了Mutex,ReentrantLock/CountDownLatch/Semphore這些同步類的實現方式都差不多,不同的地方就在獲取-釋放資源的方式tryAcquire-tryRelelase。掌握了這點,AQS的核心便被攻破了!

OK,至此,整個AQS的講解也要落下帷幕了。希望本文能夠對學習Java并發編程的同學有所借鑒,中間寫的有不對的地方,也歡迎討論和指正~

5、參考

Java并發之AQS詳解

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,312評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,993評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,410評論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,778評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,955評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,521評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,266評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,468評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,696評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,193評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,431評論 2 378

推薦閱讀更多精彩內容