带你进入java中的CountDownLatch


1.简介

在这篇文章中,我们介绍了一下 CountDownLatch类,并且演示了一下在实战案例中是如何使用的。关键地是,通过使用CountDownLatch,我们可以让一个线程阻塞直到其他线程完成了给定的任务。


2.在并发编程中的使用

简单地说,CountDownLatch有一个counter域,在我们要求的时候,你可以消减这个域。 之后,我们使用它来阻塞一个调用线程直到它被消减为零。如果我们正在做一些并行处理,那么我们就可以把CounterDownLatch的counter的值设置成和我们将要使用的线程数量一样。那时,我们就可以在每个线程结束之后调用countDown()方法,从而保证:一个调用await()的独立线程将会阻塞掉直到工作线程结束。


3.等待线程池完成

在下面的案例中,我们创建了一个Worker并且在该线程运行完成的时候使用CountDownLatch域字段发出一个信号:

publicclassWorker implementsRunnable {

    privateList outputScraper;

    privateCountDownLatch countDownLatch;

    publicWorker(List outputScraper, CountDownLatch countDownLatch) {

        this.outputScraper = outputScraper;

        this.countDownLatch = countDownLatch;

    }

    @Override

    publicvoidrun() {

        doSomeWork();

        outputScraper.add("Counted down");

        countDownLatch.countDown();

    }

}

然后,我们创建一个test来证明一下: 我们可以用一个CountDownLatch来等待Worker实例完成结束:

@Test

publicvoidwhenParallelProcessing_thenMainThreadWillBlockUntilCompletion()

  throwsInterruptedException {

    List outputScraper = Collections.synchronizedList(newArrayList<>());

    CountDownLatch countDownLatch = newCountDownLatch(5);

    List workers = Stream

      .generate(() -> newThread(newWorker(outputScraper, countDownLatch)))

      .limit(5)

      .collect(toList());

      workers.forEach(Thread::start);

      countDownLatch.await();

      outputScraper.add("Latch released");

      assertThat(outputScraper)

        .containsExactly(

          "Counted down",

          "Counted down",

          "Counted down",

          "Counted down",

          "Counted down",

          "Latch released"

        );

    }

正常情况下,“Latch released” 都将是最后一个输出-因为它依赖于CountDownLatch释放。

注意: 如果我们没有调用await()方法,那就无法保证线程的执行顺序,因此,这个测试也会随机性地失败。


4.一池子的线程等待开始

我们继续拿之前的例子来说,但是这一次,我们却要开启上千个线程而不再是5个。很可能会出现这样的情况,在我们为后面的线程调用start方法之前,前面的许多线程已经结束运行了。

这样的话,想重现一个并发问题就变得很困难了。因为,我们无法让我们的所有线程并发运行。

围绕这一点,我们使用CountdownLatch时,就和之前的例子有点不同了。相比较于 阻塞一个父线程直到一些子线程结束运行,我们可以在所有其他线程开始之前,把每一个子线程阻塞掉。我们可以阻塞每一个子线程直到其他所有线程都启动了(started)。

我们来修改一下run()方法,让他在执行之前先阻塞住:

public class   WaitingWorker implementsRunnable {

    privateList outputScraper;

    private   CountDownLatch    readyThreadCounter;

    private   CountDownLatch   callingThreadBlocker;

    private    CountDownLatch   completedThreadCounter;


    public    WaitingWorker(

      List outputScraper,

      CountDownLatch readyThreadCounter,

      CountDownLatch callingThreadBlocker,

      CountDownLatch completedThreadCounter) {

        this.outputScraper = outputScraper;

        this.readyThreadCounter = readyThreadCounter;

        this.callingThreadBlocker = callingThreadBlocker;

        this.completedThreadCounter = completedThreadCounter;

    }

    @Override

    public  void  run() {

        readyThreadCounter.countDown();

        try{

            callingThreadBlocker.await();

            doSomeWork();

            outputScraper.add("Counted down");

        } catch(InterruptedException e) {

            e.printStackTrace();

        } finally{

            completedThreadCounter.countDown();

        }

    }

}

现在,我们也修改一下我们的测试,这样,它就会先在所有的worker线程启动之前阻塞住,然后解除阻塞,之后,再次阻塞直到所有的Worker都已经结束:

@Test

public  void     whenDoingLotsOfThreadsInParallel_thenStartThemAtTheSameTime()

 throwsInterruptedException {

    List outputScraper = Collections.synchronizedList(newArrayList<>());

    CountDownLatch readyThreadCounter = newCountDownLatch(5);

    CountDownLatch callingThreadBlocker = newCountDownLatch(1);

    CountDownLatch completedThreadCounter = newCountDownLatch(5);

    List workers = Stream

      .generate(() -> newThread(newWaitingWorker(

        outputScraper, readyThreadCounter, callingThreadBlocker, completedThreadCounter)))

      .limit(5)

      .collect(toList());

    workers.forEach(Thread::start);

    readyThreadCounter.await();

    outputScraper.add("Workers ready");

    callingThreadBlocker.countDown();

    completedThreadCounter.await();

    outputScraper.add("Workers complete");

    assertThat(outputScraper)

      .containsExactly(

        "Workers ready",

        "Counted down",

        "Counted down",

        "Counted down",

        "Counted down",

        "Counted down",

        "Workers complete"

      );

}

这个模式对于重现并发bug十分有用,因为它能强制上千个线程以并行的的方式执行业务逻辑。


5.过早结束一个CountDownLatch

有时,会出现这样的情况,在递减CountDownLatch的值之前,Worker就已经因发生错误而终止。这就会导致这样一个结果:countDownLatch的值永远不会为零,await()方法也决不会结束:

@Override

publicvoidrun() {

    if(true) {

        thrownewRuntimeException("Oh dear, I'm a BrokenWorker");

    }

    countDownLatch.countDown();

    outputScraper.add("Counted down");

}

为了演示await()方法是如何永远阻塞的,我们用一个BrokenWorker来修改我们之前的例子:

@Test

public   void      whenFailingToParallelProcess_thenMainThreadShouldGetNotGetStuck()

  throws   InterruptedException {


    List outputScraper = Collections.synchronizedList(newArrayList<>());

    CountDownLatch countDownLatch = newCountDownLatch(5);

    List workers = Stream

      .generate(() -> newThread(newBrokenWorker(outputScraper, countDownLatch)))

      .limit(5)

      .collect(toList());

    workers.forEach(Thread::start);

    countDownLatch.await();

}

很明显,这并不是我们想要的行为- 相比较于无限的阻塞,让应用程序继续运行可能更好一点。为了回避这个问题,我们可以为我们的await()方法增加一个超时时间参数timeout:

booleancompleted = countDownLatch.await(3L, TimeUnit.SECONDS);

assertThat(completed).isFalse();

正如我们所看到的,测试最终将超时 ,并且await()将返回false。


6.总结

在这篇短文中,我们论证了我们如何用CountDownLatch来阻塞一个线程直到其他线程结束了一些处理。同时,我们也展示了如何使用CountDownLatch来确保线程并发运行,从而帮助于调试一些并发问题。这些案例代码都能在github上找到,这是一个基于maven的项目,所以运行起来应该很简单,sourceCode

你可能感兴趣的:(带你进入java中的CountDownLatch)