CountDownLatch是什么?

本文内容如有错误、不足之处,欢迎技术爱好者们一同探讨,在本文下面讨论区留言,感谢。

简介

CountDownLatch 在Java中是一种同步器,它允许一个线程在开始执行之前,等待一个或多个线程。

可以在程序中使用Java中的等待和通知机制来实现和CountDownLatch相同的功能 ,但是它需要大量代码,并且在第一次使用时非常困难(tricky),而使用CountDownLatch 可以使用几行代码简单完成。CountDownLatch 还允许灵活地等待主线程要等待的线程数,它可以等待一个线程或n个线程,代码上没有太大变化。关键是需要明白Java应用程序在哪里使用CountDownLatch更好。

例如,应用程序的主线程要等待,直到负责启动框架服务的其他服务线程完成了所有服务的启动。

原理

CountDownLatch的工作原理是使用线程数初始化计数器,每次线程执行完成时,计数器都会递减。当计数器个数(count)达到零时,表示所有线程已完成其执行,并且等待latch锁的线程(例如:主线程)将恢复执行。

在这里插入图片描述
从图片上可以看到,流程是TA线程调用其他三个线程,等待其他3个线程执行完成后,才执行TA线程剩下的逻辑。

过程如下

  1. 主线程启动
  2. 创建包含N个线程的CountDownLatch
  3. 启动N个线程
  4. 主线程等待N个线程执行完毕
  5. N个线程完成返回
  6. 主线程恢复执行
使用

CountDownLatch.java类的构造函数:

// 根据count创造初始化一个CountDownLatch
public CountDownLatch(int count) {...}

此计数count本质上是CountDownLatch 应等待的线程数。该值只能设置一次,并且CountDownLatch 没有提供其他机制来重置此count。

使用CountDownLatch 时需要注意的两点是:

  1. 一旦计数达到零,就不能重用CountDownLatch ,这是CountDownLatch和CyclicBarrier之间的主要区别。
  2. 主线程通过调用 CountDownLatch.await()方法来等待latch线程完成,而其他线程则调用CountDownLatch.countDown()来通知它们已完成。

例子:

import java.util.concurrent.CountDownLatch; 
  
public class CountDownLatchDemo 
{ 
    public static void main(String args[])  
                   throws InterruptedException 
    { 
        // 创建一个线程等待其他4个线程执行完毕后再执行 
        CountDownLatch latch = new CountDownLatch(4); 
  
        // 创建并启动4个线程
        Worker first = new Worker(1000, latch,  
                                  "WORKER-1"); 
        Worker second = new Worker(2000, latch,  
                                  "WORKER-2"); 
        Worker third = new Worker(3000, latch,  
                                  "WORKER-3"); 
        Worker fourth = new Worker(4000, latch,  
                                  "WORKER-4"); 
        first.start(); 
        second.start(); 
        third.start(); 
        fourth.start(); 
        // main-task 等待上面4个线程
        latch.await(); 
  
        // main-thread 开始工作
        System.out.println(Thread.currentThread().getName() + 
                           " 已经完成"); 
    } 
} 
  
// 资源类 
class Worker extends Thread 
{ 
    private int delay; 
    private CountDownLatch latch; 
    public Worker(int delay, CountDownLatch latch, 
                                    String name) 
    { 
        super(name); 
        this.delay = delay; 
        this.latch = latch; 
    } 
  
    @Override
    public void run() 
    { 
        try
        { 
            Thread.sleep(delay); 
            latch.countDown(); 
            System.out.println(Thread.currentThread().getName() 
                            + " 完成"); 
        } 
        catch (InterruptedException e) 
        { 
            e.printStackTrace(); 
        } 
      }
}
使用CountDownLatch 模拟并发错误出现的场景

实际项目中启动了数千个线程,而不是四个线程,那么许多较早执行的线程可能已经完成处理,甚至后面的线程还没有调用start()方法的时候。这可能使尝试重现并发问题变得困难,因为无法使所有线程并行运行。

为了解决这个问题,使CountdownLatch 的工作方式与前面的示例不同。除了在某些子线程完成之前阻塞父线程之外,需要在每个子线程都启动之前阻塞每个子线程。等所有线程都到位并进行等待的时候,释放这些等待的线程,这就好比1000个人参加100米跑步,所有人都站在起跑线上等待起跑枪声,枪声一响,所有运动员开始比赛,并发跑步。

修改run() 方法,使其在处理之前阻塞:

public class WaitingWorker implements Runnable {
 
 	// 输出黑板
    private List<String> outputScraper;
    // 准备线程数latch
    private CountDownLatch readyThreadCounter;
    // 调用线程数latch
    private CountDownLatch callingThreadBlocker;
    // 计算完成线程数latch
    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();
        }
    }
}

下面的测试类,main方法阻塞直到所有Workers启动,然后解除阻塞,然后再阻塞直到Workers完成:

@Test
public void whenDoingLotsOfThreadsInParallel_thenStartThemAtTheSameTime()
 throws InterruptedException {
  
  	// ArrayList是线程不安全类,需要调用Collections.synchronizedList()进行线程安全处理。
    List<String> outputScraper = Collections.synchronizedList(new ArrayList<>());
    CountDownLatch readyThreadCounter = new CountDownLatch(4);
    CountDownLatch callingThreadBlocker = new CountDownLatch(1);
    CountDownLatch completedThreadCounter = new CountDownLatch(4);
    // Stream流,生成4个线程数
    List<Thread> workers = Stream
      .generate(() -> new Thread(new WaitingWorker(
        outputScraper, readyThreadCounter, callingThreadBlocker, completedThreadCounter)))
      .limit(4)
      .collect(toList());
 
    workers.forEach(Thread::start);
    readyThreadCounter.await(); 
    outputScraper.add("Workers ready");
    // 启动所有线程操作,将调用线程latch进行countDown解除所有线程等待,相当于跑步时的裁判发出的枪声
    callingThreadBlocker.countDown(); 
    completedThreadCounter.await(); 
    outputScraper.add("Workers complete");
 
    assertThat(outputScraper)
      .containsExactly(
        "Workers ready",
        "Counted down",
        "Counted down",
        "Counted down",
        "Counted down",
        "Workers complete"
      );
}

这种方法可以用来迫使成千上万的线程尝试并行执行某些逻辑,因此这种模式对于尝试重现并发错误非常有用。

超时调用处理

上面的代码,可以看到对中断异常进行了try-catch,因为有些线程会发生中断或者其他异常,如果某个线程的异常如下:

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

那么,调用这个latch的线程将一直进行等待,无法继续执行下去,因为CountDownLatch 中的计数器Counter永远不可能为0,因此需要在await()的调用中添加一个超时参数。

boolean completed = countDownLatch.await(3L, TimeUnit.SECONDS);
使用场景
  1. 实现最大并行测试,上面的例子中已经给出。
  2. 等待N个线程完成,然后再开始执行。
  3. 死锁检测,一个非常方便的用例,可以在每个测试阶段使用N个线程访问具有不同数量线程的共享资源,试图创建死锁进行测试。
参考资料

How is CountDownLatch used in Java Multithreading? (在Java多线程中如何使用CountDownLatch?

Guide to CountDownLatch in Java (CountDownLatch指南

CountDownLatch in Java (Java中的CountDownLatch)

Java concurrency – CountDownLatch Example(CountDownLatch案例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值