CountdownLatch詳解

目錄

CountdownLatch

核心屬性

構造函數

核心方法


文章轉載自:https://segmentfault.com/a/1190000016508108

CountdownLatch

CountDownLatch本身是基於共享鎖實現的,該工具是爲了解決某些操作只能在一組操作全部執行完成後才能執行的情景CountDown是倒數計,所以CountDownLatch的用法通常是設定一個大於0的值,該值即代表需要等待的總任務數,每完成一個任務後,將總任務數減一,直到最後該值爲0,說明所有等待的任務都執行完了,“門閂”此時就被打開,後面的任務可以繼續執行。

舉個例子:一個app中三個線程同時進行飛機票查詢,設置CountDown初值爲3,每結束一個線程CountDown減1,CountDown爲0時統計機票數並輸出。在CountDown大於0之前,所有已完成任務的進程都會被阻塞,CountDown爲0後,由latch.await()方法喚醒被阻塞的進程繼續執行latch.await()方法後續方法。

先看下面例子,後面再詳細分析

public class FightQueryDemo {

    private static List<String> company= Arrays.asList("東方航空","南方航空","海南航空");
    private static List<String> fightList=new ArrayList<>();

    public static void main(String[] args) throws InterruptedException{
        String origin="BJ";
        String dest="SH";
        Thread[] threads=new Thread[company.size()];
        CountDownLatch latch=new CountDownLatch(company.size());

        for (int i = 0; i < threads.length; i++) {
            String name=company.get(i);
            threads[i]=new Thread(()->{
                System.out.printf("%s 查詢從%s到%s的機票\n",name,origin,dest);
                //隨機產生票數
                int val=new Random().nextInt(10);
                try {
                    TimeUnit.SECONDS.sleep(val);
                    fightList.add(name+"--"+val);
                    System.out.printf("%s公司查詢成功!\n",name);
                    latch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }
        latch.await();
        System.out.println("==============查詢結果如下:================");
        fightList.forEach(System.out::println);
    }
}

核心屬性

CountDownLatch主要是通過AQS的共享鎖機制實現的,因此它的核心屬性只有一個sync,它繼承自AQS,同時覆寫了tryAcquireSharedtryReleaseShared,以完成具體的實現共享鎖的獲取與釋放的邏輯。

private final Sync sync;
private static final class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 4982264981922014374L;

    Sync(int count) {
        setState(count);
    }

    int getCount() {
        return getState();
    }

    protected int tryAcquireShared(int acquires) {
        return (getState() == 0) ? 1 : -1;
    }

    protected boolean tryReleaseShared(int releases) {
        // Decrement count; signal when transition to zero
        for (;;) {
            int c = getState();
            if (c == 0)
                return false;
            int nextc = c-1;
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }
}

構造函數

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

在構造函數中,我們就是簡單傳入了一個不小於0的任務數,由上面Sync的構造函數可知,這個任務數就是AQS的state的初始值。 

核心方法

CountDownLatch最核心的方法只有兩個,一個是countDown方法,每調用一次,就會將當前的count減一,當count值爲0時,就會喚醒所有等待中的線程;另一個是await方法,它有兩種形式,一種是阻塞式,一種是帶超時機制的形式,該方法用於將當前等待“門閂”開啓的線程掛起,直到count值爲0,這一點很類似於條件隊列,相當於等待的條件就是count值爲0,然而其底層的實現並不是用條件隊列,而是共享鎖。

countDown()

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

前面說過,countDown()方法的目的就是將count值減一,並且在count值爲0時,喚醒所有等待的線程,它內部調用的其實是釋放共享鎖的操作:

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

該方法由AQS實現,但是tryReleaseShared方法由Sync類自己實現:

protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

該方法的實現很簡單,就是獲取當前的state值,如果已經爲0了,直接返回false;否則通過CAS操作將state值減一,之後返回的是nextc == 0,由此可見,該方法只有在count值原來不爲0,但是調用後變爲0時,纔會返回true,否則返回false,並且也可以看出,該方法在返回true之後,後面如果再次調用,還是會返回false。也就是說,調用該方法只有一種情況會返回true,那就是state值從大於0變爲0值時,這時也是所有在門閂前的任務都完成了。

在tryReleaseShared返回true以後,將調用doReleaseShared方法喚醒所有等待中的線程,該方法我們在前面的文章中已經詳細分析過了,這裏就不再贅述了。

值得一提的是,我們其實並不關心releaseShared的返回值,而只關心tryReleaseShared的返回值,或者只關心count到0了沒有,這裏更像是借了共享鎖的“殼”,來完成我們的目的,事實上我們完全可以自己設一個全局變量count來實現相同的效果,只不過對這個全局變量的操作也必須使用CAS。

await()

與Condition的await()方法的語義相同,該方法是阻塞式地等待,並且是響應中斷的,只不過它不是在等待signal操作,而是在等待count值爲0:

可見,await方法內部調用的是acquireSharedInterruptibly方法,相當於借用了獲取共享鎖的“殼”:

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

我們來回憶一下獨佔模式下對應的方法:

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

可見,兩者用的是同一個框架,只是這裏:

         tryAcquire(arg) 換成了 tryAcquireShared(arg) (子類實現)
         doAcquireInterruptibly(arg) 換成了 doAcquireSharedInterruptibly(arg) (AQS提供)


我們先來看看Sync子類對於tryAcquireShared的實現:

protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

該方法似乎有點掛羊頭賣狗肉的感覺——所謂的獲取共享鎖,事實上並不是什麼搶鎖的行爲,沒有任何CAS操作,它就是判斷當前的state值是不是0,是就返回1,不是就返回-1。

值得注意的是,在逐行分析AQS源碼(3)——共享鎖的獲取與釋放中我們特別提到過tryAcquireShared返回值的含義:

  • 如果該值小於0,則代表當前線程獲取共享鎖失敗
  • 如果該值大於0,則代表當前線程獲取共享鎖成功,並且接下來其他線程嘗試獲取共享鎖的行爲很可能成功
  • 如果該值等於0,則代表當前線程獲取共享鎖成功,但是接下來其他線程嘗試獲取共享鎖的行爲會失敗

所以,當該方法的返回值不小於0時,就說明搶鎖成功,可以直接退出了,所對應的就是count值已經爲0,所有等待的事件都滿足了。否則,我們調用doAcquireSharedInterruptibly(arg)將當前線程封裝成Node,丟到sync queue中去阻塞等待:

private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

await(long timeout, TimeUnit unit)

相較於await()方法,await(long timeout, TimeUnit unit)提供了超時等待機制:

public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    return tryAcquireShared(arg) >= 0 || doAcquireSharedNanos(arg, nanosTimeout);
}
private boolean doAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    final long deadline = System.nanoTime() + nanosTimeout;
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
            }
            nanosTimeout = deadline - System.nanoTime();
            if (nanosTimeout <= 0L)
                return false;
            if (shouldParkAfterFailedAcquire(p, node) &&
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

注意,在tryAcquireSharedNanos方法中,我們用到了doAcquireSharedNanos的返回值,如果該方法因爲超時而退出時,則將返回false。由於await()方法是阻塞式的,也就是說沒有獲取到鎖是不會退出的,因此它沒有返回值,換句話說,如果它正常返回了,則一定是因爲獲取到了鎖而返回; 而await(long timeout, TimeUnit unit)由於有了超時機制,它是有返回值的,返回值爲true則表示獲取鎖成功,爲false則表示獲取鎖失敗。doAcquireSharedNanos的這個返回值有助於我們理解該方法究竟是因爲獲取到了鎖而返回,還是因爲超時時間到了而返回。

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