深入淺出 Java Concurrency (10): 鎖機制 part 5 閉鎖 (CountDownLatch)

來源:互聯網
上載者:User

標籤:art   public   park   進度   隊列   例子   獨立   標識   失效   

此小節介紹幾個與鎖有關的有用工具。

閉鎖(Latch)

閉鎖(Latch):一種同步方法,可以延遲線程的進度直到線程到達某個終點狀態。通俗的講就是,一個閉鎖相當於一扇大門,在大門開啟之前所有線程都被阻斷,一旦大門開啟所有線程都將通過,但是一旦大門開啟,所有線程都通過了,那麼這個閉鎖的狀態就失效了,門的狀態也就不能變了,只能是開啟狀態。也就是說閉鎖的狀態是一次性的,它確保在閉鎖開啟之前所有特定的活動都需要在閉鎖開啟之後才能完成。

CountDownLatch是JDK 5+裡面閉鎖的一個實現,允許一個或者多個線程等待某個事件的發生。CountDownLatch有一個正數計數器,countDown方法對計數器做減操作,await方法等待計數器達到0。所有await的線程都會阻塞直到計數器為0或者等待線程中斷或者逾時。

CountDownLatch的API如下。

  • public void await() throws InterruptedException
  • public boolean await(long timeout, TimeUnit unit) throws InterruptedException
  • public void countDown()
  • public long getCount()

其中getCount()描述的是當前計數,通常用於調試目的。

下面的例子中描述了閉鎖的兩種常見的用法。

package xylz.study.concurrency.lock;

import java.util.concurrent.CountDownLatch;

public class PerformanceTestTool {

    public long timecost(final int times, final Runnable task) throws InterruptedException {
        if (times <= 0) throw new IllegalArgumentException();
        final CountDownLatch startLatch = new CountDownLatch(1);
        final CountDownLatch overLatch = new CountDownLatch(times);
        for (int i = 0; i < times; i++) {
            new Thread(new Runnable() {
                public void run() {
                    try {
                        startLatch.await();
                        //
                        task.run();
                    } catch (InterruptedException ex) {
                        Thread.currentThread().interrupt();
                    } finally {
                        overLatch.countDown();
                    }
                }
            }).start();
        }
        //
        long start = System.nanoTime();
        startLatch.countDown();
        overLatch.await();
        return System.nanoTime() - start;
    }

}

在上面的例子中使用了兩個閉鎖,第一個閉鎖確保在所有線程開始執行任務前,所有準備工作都已經完成,一旦準備工作完成了就調用startLatch.countDown()開啟閉鎖,所有線程開始執行。第二個閉鎖在於確保所有任務執行完成後主線程才能繼續進行,這樣保證了主線程等待所有任務線程執行完成後才能得到需要的結果。在第二個閉鎖當中,初始化了一個N次的計數器,每個任務執行完成後都會將計數器減一,所有任務完成後計數器就變為了0,這樣主線程閉鎖overLatch拿到此訊號後就可以繼續往下執行了。

根據前面的happend-before法則可以知道閉鎖有以下特性:

記憶體一致性效果:線程中調用 countDown() 之前的操作 happen-before 緊跟在從另一個線程中對應 await() 成功返回的操作。

在上面的例子中第二個閉鎖相當於把一個任務拆分成N份,每一份獨立完成任務,主線程等待所有任務完成後才能繼續執行。這個特性在後面的線程池架構中會用到,其實FutureTask就可以看成一個閉鎖。後面的章節還會具體分析FutureTask的。

 

同樣基於探索精神,仍然需要“窺探”下CountDownLatch裡面到底是如何?await*countDown的。

首先,研究下await()方法。內部直接調用了AQSacquireSharedInterruptibly(1)

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

前面一直提到的都是獨佔鎖(排它鎖、互斥鎖),現在就用到了另外一種鎖,共用鎖定。

所謂共用鎖定是說所有共用鎖定的線程共用同一個資源,一旦任意一個線程拿到共用資源,那麼所有線程就都擁有的同一份資源。也就是通常情況下共用鎖定只是一個標誌,所有線程都等待這個標識是否滿足,一旦滿足所有線程都被啟用(相當於所有線程都拿到鎖一樣)。這裡的閉鎖CountDownLatch就是基於共用鎖定的實現。

閉鎖中關於AQStryAcquireShared的實現是如下代碼(java.util.concurrent.CountDownLatch.Sync.tryAcquireShared):

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

在這份邏輯中,對於閉鎖而言第一次await時tryAcquireShared應該總是-1,因為對於閉鎖CountDownLatch而言state的值就是初始化的count值。這也就解釋了為什麼在countDown調用之前閉鎖的count總是>0。

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    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
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                break;
        }
    } catch (RuntimeException ex) {
        cancelAcquire(node);
        throw ex;
    }
    // Arrive here only if interrupted
    cancelAcquire(node);
    throw new InterruptedException();
}

上面的邏輯展示了如何通過await將所有線程串聯並掛起,直到被喚醒或者條件滿足或者被中斷。整個過程是這樣的:

  1. 將當前線程節點以共用模式加入AQSCLH隊列中(相關概念參考這裡和這裡)。進行2。
  2. 檢查當前節點的前任節點,如果是頭結點並且當前閉鎖計數為0就將當前節點設定為頭結點,喚醒繼任節點,返回(結束線程阻塞)。否則進行3。
  3. 檢查線程是否該阻塞,如果應該就阻塞(park),直到被喚醒(unpark)。重複2。
  4. 如果2、3有異常就拋出異常(結束線程阻塞)。

這裡有一點值得說明下,設定頭結點並喚醒繼任節點setHeadAndPropagate。由於前面tryAcquireShared總是返回1或者-1,而進入setHeadAndPropagate時總是propagate>=0,所以這裡propagate==1。後面喚醒繼任節點操作就非常熟悉了。

private void setHeadAndPropagate(Node node, int propagate) {
    setHead(node);
    if (propagate > 0 && node.waitStatus != 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            unparkSuccessor(node);
    }
}

從上面的所有邏輯可以看出countDown應該就是在條件滿足(計數為0)時喚醒頭結點(時間最長的一個節點),然後頭結點就會根據FIFO隊列喚醒整個節點列表(如果有的話)。

CountDownLatchcountDown代碼中看到,直接調用的是AQSreleaseShared(1),參考前面的知識,這就印證了上面的說法。

tryReleaseShared中正是採用CAS操作減少計數(每次減-1)。

public boolean tryReleaseShared(int releases) {
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

整個CountDownLatch就是這個樣子的。其實有了前面原子操作和AQS的原理及實現,分析CountDownLatch還是比較容易的。

深入淺出 Java Concurrency (10): 鎖機制 part 5 閉鎖 (CountDownLatch)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.