併發工具類(一)等待多執行緒的CountDownLatch

王小胖醬發表於2019-01-22

前言

  JDK中為了處理執行緒之間的同步問題,除了提供鎖機制之外,還提供了幾個非常有用的併發工具類:CountDownLatch、CyclicBarrier、Semphore、Exchanger、Phaser;
  CountDownLatch、CyclicBarrier、Semphore、Phaser 這四個工具類提供一種併發流程的控制手段;而Exchanger工具類則提供了線上程之間交換資料的一種手段。

簡介

  CountDownLatch 允許一個或多個執行緒等待其他執行緒完成操作。單詞Latch的意思是“門閂”,所以沒有開啟時,N個人是不能進入屋內的,也就是N個執行緒是不能往下執行的,從而控制執行緒執行任務的時機,使執行緒以“組團”的方式一起執行任務。
  CountDownLatch 類 在建立時,給定一個計數count。執行緒呼叫CountDownLatch 物件的awiat( )方法時,判斷這個計數count是否為0,如果不為0,就進入等待狀態。其他執行緒在完成一定任務時,呼叫CountDownLatch 的countDown()方法,使計數count減一。直到count的值等於0或者少於0時,便是等待執行緒的執行時機,將會繼續往下執行。

CountDownLatch的API介面

方法名稱描 述
void await()使當前執行緒在鎖存器倒計數至零之前一直等待,除非執行緒被中斷。
boolean await(long timeout, TimeUnit unit)使當前執行緒在鎖存器倒計數至零之前一直等待,除非執行緒被中斷或超出了指定的等待時間。
void countDown()遞減鎖存器的計數,如果計數到達零,則釋放所有等待的執行緒。
long getCount()返回當前計數。
String toString()返回標識此鎖存器及其狀態的字串。

注意:
await()也可以被多個執行緒同時呼叫,從而實現多個執行緒 等待其他的多個執行緒完成某部分操作。

下面是API文件介紹的兩個經典用法:

@ Example1

Driver類中建立了一組worker 執行緒,所有的worker執行緒必須等待Driver類完成初始化動作,才能往下執行。完成初始化動作後,Driver類也必須等待所有worker執行緒完成才能結束。本例子中使用了兩個CountDownLatch類:

  • startSignal是一個啟動訊號,在 driver 為繼續執行 worker 做好準備之前,它會阻止所有的 worker 繼續執行。
  • doneSignal是一個完成訊號,它允許 driver 在完成所有 worker 之前一直等待。

class Driver { // ...
   void main() throws InterruptedException {
     CountDownLatch startSignal = new CountDownLatch(1);
     CountDownLatch doneSignal = new CountDownLatch(N);

     for (int i = 0; i < N; ++i) // create and start threads
       new Thread(new Worker(startSignal, doneSignal)).start();

     doSomethingElse();            // don't let run yet
     startSignal.countDown();      // let all threads proceed
     doSomethingElse();
     doneSignal.await();           // wait for all to finish
   }
 }

 class Worker implements Runnable {
   private final CountDownLatch startSignal;
   private final CountDownLatch doneSignal;
   Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
      this.startSignal = startSignal;
      this.doneSignal = doneSignal;
   }
   public void run() {
      try {
        startSignal.await();
        doWork();
        doneSignal.countDown();
} catch (InterruptedException ex) {} // return;
   }

   void doWork() { ... }
 }
複製程式碼

@ Example2

:另一種典型用法是,將一個問題分成 N 個部分(N個小任務),然後將這些任務Runnable交由執行緒池來完成,每個子任務執行完成,就計數一次,主執行緒則等待這些子任務完成。當所有的子部分完成後,主執行緒就能夠通過 await。(當執行緒必須用這種方法反覆倒計數時,可改為使用 CyclicBarrier。)

class Driver2 { // ...
   void main() throws InterruptedException {
     CountDownLatch doneSignal = new CountDownLatch(N);
     Executor e = ...

     for (int i = 0; i < N; ++i) // create and start threads
       e.execute(new WorkerRunnable(doneSignal, i));

     doneSignal.await();           // wait for all to finish
   }
 }

 class WorkerRunnable implements Runnable {
   private final CountDownLatch doneSignal;
   private final int i;
   WorkerRunnable(CountDownLatch doneSignal, int i) {
      this.doneSignal = doneSignal;
      this.i = i;
   }
   public void run() {
      try {
        doWork(i);
        doneSignal.countDown();
      } catch (InterruptedException ex) {} // return;
   }

   void doWork() { ... }
 }
複製程式碼

應用場景

  假如有這樣一個需求,當我們需要解析一個Excel裡多個sheet的資料時,可以考慮使用多執行緒,每個執行緒解析一個sheet裡的資料,等到所有的sheet都解析完之後,程式需要提示解析完成。在這個需求中,要實現主執行緒等待所有執行緒完成sheet的解析操作,最簡單的做法是使用join。程式碼如下:

public class JoinCountDownLatchTest {

    public static void main(String[] args) throws InterruptedException {
        Thread parser1 = new Thread(new Runnable() {
            @Override
            public void run() {
            }
        });

        Thread parser2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("parser2 finish");
            }
        });

        parser1.start();
        parser2.start();
        parser1.join();
        parser2.join();
        System.out.println("all parser finish");
    }

}
複製程式碼

join用於讓當前執行執行緒等待join執行緒執行結束。其實現原理是不停檢查join執行緒是否存活,如果join執行緒存活則讓當前執行緒永遠wait,程式碼片段如下,wait(0)表示永遠等待下去。

while (isAlive()) {
 wait(0);
}
複製程式碼

直到join執行緒中止後,執行緒的this.notifyAll會被呼叫,呼叫notifyAll是在JVM裡實現的,所以JDK裡看不到,有興趣的同學可以看看JVM原始碼。JDK不推薦線上程例項上使用wait,notify和notifyAll方法。
而在JDK1.5之後的併發包中提供的

CountDownLatch也可以實現join的這個功能,並且比join的功能更多。

public class CountDownLatchTest {

    static CountDownLatch c = new CountDownLatch(2);

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(1);
                c.countDown();
                System.out.println(2);
                c.countDown();
            }
        }).start();

        c.await();
        System.out.println("3");
    }

}
複製程式碼

CountDownLatch的建構函式接收一個int型別的引數作為計數器,如果你想等待N個點完成,這裡就傳入N。

當我們呼叫一次CountDownLatch的countDown方法時,N就會減1,CountDownLatch的await會阻塞當前執行緒,直到N變成零。

由於countDown方法可以用在任何地方,所以這裡說的N個點,可以是N個執行緒,也可以是1個執行緒裡的N個執行步驟

。用在多個執行緒時,你只需要把這個CountDownLatch的引用傳遞到執行緒裡。

其他方法:

如果有某個解析sheet的執行緒處理的比較慢,我們不可能讓主執行緒一直等待,所以我們可以使用另外一個帶指定時間的await方法,await(long time, TimeUnit unit): 這個方法等待特定時間後,就會不再阻塞當前執行緒。join也有類似的方法。

注意:

  • 計數器必須大於等於0,只是等於0時候,計數器就是零,呼叫await方法時不會阻塞當前執行緒。CountDownLatch不可能重新初始化或者修改CountDownLatch物件的內部計數器的值。
  • 一個執行緒呼叫countDown方法 happen-before 另外一個執行緒呼叫await方法。

CountDownLatch 的原始碼分析

最後,我們簡單看一下 CountDownLatch是怎麼實現的:

public class CountDownLatch {
 private final Sync sync;

 public CountDownLatch(int count) {//構造器
         //count少於0將丟擲異常
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

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

 public void countDown() {
        sync.releaseShared(1);
    }
//........
}
複製程式碼

在建立countDownLatch,其構造器裡面建立了一個sync類,並且await()countDown方法都是都是通過此類來實現的。

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

        Sync(int count) {
          //設定state的值為countDownLatch的計數的數目
            setState(count);
        }

        int getCount() {
            return getState();
        }

        //如果state值為0.也就是計數完成了,就不可以再獲取共享鎖,這也是為什麼CountLatch只能用一次
        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; //狀態state減一
                if (compareAndSetState(c, nextc))
                    return nextc == 0;//計數到0了,表示釋放鎖成功。
            }
        }
    }
複製程式碼

與大部分的併發工具類一樣,都是繼承使用了JDK提供的強大的AQS框架類AbstractQueuedSynchronizer,而且使用的還是共享鎖,共享鎖能允許執行緒進入的執行緒數目,就是CountDownLatch傳入的引數。


 文章源地址:https://www.cnblogs.com/jinggod/p/8492067.html


相關文章