JUC并发工具包之CountDownLatch

1、介绍

本文将介绍CountDownLatch并给出实践中的几个例子,通过使用CountDownLatch我们可以让一个线程阻塞直到其他一个或多个线程执行完成。

A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.

2、并发编程中的用法

简单的说,CountDownLatch有一个counter属性,它可以按照我们需求递减,我们可以使用它去阻塞一个正被调用的线程直到counter递减为0。

如果我们在做一些并行的处理,我们可以初始化CountDownLatch的counter值为我们即将要运行的线程数,我们可以在每个线程运行结束的时候调用它的countdown方法,这时当前主线程调用await方法会阻塞直到其他worker线程都完成。

3、等待多个线程处理完毕

我们现在尝试通过创建一个Worker线程并设置一个CountDownLatch属性,在线程执行完毕的时候可以通知到我们。

public class Worker implements Runnable {
    private List<String> outputScraper;
    private CountDownLatch countDownLatch;
 
    public Worker(List<String> outputScraper, CountDownLatch countDownLatch) {
        this.outputScraper = outputScraper;
        this.countDownLatch = countDownLatch;
    }
 
    @Override
    public void run() {
        doSomeWork();
        outputScraper.add("Counted down");
        countDownLatch.countDown();
    }
}

我们现在创建一个测试用例来验证主线程会阻塞到工作线程完成。

@Test
public void whenParallelProcessing_thenMainThreadWillBlockUntilCompletion()
  throws InterruptedException {
 
    List<String> outputScraper = Collections.synchronizedList(new ArrayList<>());
    CountDownLatch countDownLatch = new CountDownLatch(5);
    List<Thread> workers = Stream
      .generate(() -> new Thread(new Worker(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个,其中靠前运行的线程很有可能在我们还没运行靠后线程的时候就已经结束了,这就让我们很难去复现一些并发问题,因为我们没有办法让所有的线程并行开始执行。

为了解决这个问题,这次我们使用CountDownLatch的方式和上面几个例子不太一样,相比阻塞主线程等待子线程运行结束,我们可以阻塞所有子线程直到所有子线程都启动完成。

我们修改上例的run方法让它在正式运行前一直阻塞:

public class WaitingWorker implements Runnable {
 
    private List<String> outputScraper;
    private CountDownLatch readyThreadCounter;
    private CountDownLatch callingThreadBlocker;
    private CountDownLatch completedThreadCounter;
 
    public WaitingWorker(
      List<String> 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();
        }
    }
}

现在我们再修改我们的测试用例,让所有子线程正式执行前一直阻塞,然后打开阻塞让所有子线程运行,最后主线程阻塞直至所有子线程执行完毕。

@Test
public void whenDoingLotsOfThreadsInParallel_thenStartThemAtTheSameTime()
 throws InterruptedException {
  
    List<String> outputScraper = Collections.synchronizedList(new ArrayList<>());
    CountDownLatch readyThreadCounter = new CountDownLatch(5);
    CountDownLatch callingThreadBlocker = new CountDownLatch(1);
    CountDownLatch completedThreadCounter = new CountDownLatch(5);
    List<Thread> workers = Stream
      .generate(() -> new Thread(new WaitingWorker(
        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"
      );
}

这种模式对重现并发问题非常有用,因为它可以让上千个线程并行执行某一段逻辑。

5、提前结束CountDownLatch

有时我们会碰到在countdown()方法执行前线程终止了,这会导致CountDownLatch永远递减不到0最终导致主线程一直阻塞:

@Override
public void run() {
    if (true) {
        throw new RuntimeException("Oh dear, I'm a BrokenWorker");
    }
    countDownLatch.countDown();
    outputScraper.add("Counted down");
}

我们来修改下之前的测试用例来说明await()方法会一直阻塞:

@Test
public void whenFailingToParallelProcess_thenMainThreadShouldGetNotGetStuck()
  throws InterruptedException {
  
    List<String> outputScraper = Collections.synchronizedList(new ArrayList<>());
    CountDownLatch countDownLatch = new CountDownLatch(5);
    List<Thread> workers = Stream
      .generate(() -> new Thread(new BrokenWorker(outputScraper, countDownLatch)))
      .limit(5)
      .collect(toList());
 
    workers.forEach(Thread::start);
    countDownLatch.await();
}

很明显,这不是我们想要的结果,程序还是得继续往前执行而不是永无止境的阻塞。

为了解决这个问题,我们在调用await()方法的时候加上一个timeout的参数:

boolean completed = countDownLatch.await(3L, TimeUnit.SECONDS);
assertThat(completed).isFalse();

这样测试用例最终会超时且await()会返回false。

6、总结

  • 我们展示了怎么使用CountDownLatch来阻塞主线程直至其他线程执行完毕。
  • 我们还展示了它可以帮我们使多个线程并行运行来调试并发问题。

最后这些例子的实现都可以在Github上找到

猜你喜欢

转载自blog.csdn.net/HalfImmortal/article/details/106771394
今日推荐