CountDownLatch简介
有这么一种场景,某个线程需要等待其他线程任务完成后才能继续执行,比如,需要统计计算三个sheet页面上的数据,于是我就需要开启三个线程来做这个事情,一个线程计算一个sheet页,最后三个线程都计算完了再汇总结果。java.util.concurrent包下的CountDownLatch允许一个或多个线程等待,直到其它线程组操作完成(就是N计数器变为0)后才执行,便可以解决这个问题。
CountDownLatch在业界也被称为闭锁,它通过接收一个带int值N的形参构造器来作为计数器,每调用一次CountDownLatch.countDown()方法N就会递减1,同时CountDownLatch.await()方法会阻塞当前线程,直到N=0为止。计数器是一次性行为,不能被重置,若需要重置count,则考虑使用CyclicBarrier。
注意:如下代码可以看到,N的值必须大于0,可以等于0(说明计数器就为0,此时调用await()方法不会阻塞当前线程),同时要保证计数器N在线程通过之前就先调用。
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
CountDownLatch使用demo
final CountDownLatch countdown = new CountDownLatch(1);
for (int i = 0; i < 10; ++ i){
Thread thread= new Thread() {
public void run() {
countdown.await(); //all threads waiting
System.out.println("dosomething");
}
};
thread.start();
}
System.out.println("Begining");
countdown.countDown(); //all threads start now!
由CountDownLatch的特性可知,其非常适合用来进行多线程分组计算,然后汇总统计结果。比如常见的一个面试题:假如有Thread1、Thread2、Thread3、Thread4四条线程分别统计C、D、E、F四个盘的大小,所有线程都统计完毕交给Thread5线程去做汇总,应当如何实现?学完CountDownLatch后,你就应该有思路了(方法不局限于CountDownLatch,还有其它方法:fork/join、CyclicBarrier等)。这里结合开头说到的三个线程分别统计三个sheet页的数据,然后主线程合并统计结果为例,实现的伪代码如下:
import org.springframework.stereotype.Service;
import java.util.concurrent.*;
@Service
public class CountDownLatchService {
public static void metric() throws ExecutionException, InterruptedException {
ExecutorService executorService = new ThreadPoolExecutor(3, 3, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10));
final CountDownLatch countDownLatch = new CountDownLatch(3);
Future<Integer> submit = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
//由于我这里有return,所以我在执行method方法逻辑之前就先countDown()了,我这里还不够细
countDownLatch.countDown();
return method1();
}
});
Future<Integer> submit1 = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
countDownLatch.countDown();
return method2();
}
});
Future<Integer> submit2 = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// Thread.sleep(10000);
countDownLatch.countDown();
return method3();
}
});
countDownLatch.await();
/*
boolean await = countDownLatch.await(2, TimeUnit.SECONDS);
System.out.println("主线程等待2s的结果=" + await);
*/
Integer one = 0;
if (submit.isDone()) {
one = submit.get();
}
Integer two = 0;
if (submit1.isDone()) {
two = submit1.get();
}
Integer three = 0;
if (submit2.isDone()) {
three = submit2.get();
}
System.out.println(one + two + three);
executorService.shutdown();
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
metric();
}
public static Integer method1() {
System.out.println("我是method1");
return 1;
}
public static Integer method2() {
System.out.println("我是method2");
return 2;
}
public static Integer method3() {
System.out.println("我是method3");
return 3;
}
}
注意:当线程执行的某个任务耗费时间过长,其他任务由于CountDownLatch.await()方法就处于一直等待的状态,这是不合理的,这时可以调用如下方法来指定等待时长
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
当超过timeou等待时间后,主线程便不再等待,直接合并已经计算完的结果,测试代码把上面的32、39~42行放开,38行注释掉即可验证。也就是说,使用await设定了主线程等待时间2s后,当某个任务执行的时间超过了2s,主线程便不会继续阻塞,而是"跳过"这个任务的结果。注意:我这边获取子线程的计算结果时候,使员工isDone()方法判断计算子线程任务是否执行完成,也就是说避免了直接使用FutureTask.get()方法带来的可能阻塞的问题。
从上面来看,CountDownLatch适用的场景很多,比如多线程下执行任务,然后主线程获取且合并任务结果。需要注意线程池的创建和线程阻塞(await方法)的正确使用。
CyclicBarrer和CountDownLatc区别
1、CyclicBarrer的某个线程运行到屏障点后,线程立即停止运行,进入等待状态,直到所有的线程都达到了屏障点处,所有线程才继续运行;CountDownLatch则是当线程线程运行到某个点后,进行计数减1,该线程会继续运行;
2、CyclicBarrer可重复用,CountDownLatch则不可以;
3、CyclicBarrer通过减计数方式,计数到达指定值时释放所有等待的线程,CountDownLatch通过加计数方式,计数为0时释放所有等待的线程;
4、CountDownLatch调用countDown()方法计数减一,调用await()方法只进行阻塞,对计数没任何影响,CyclicBarrer调用await()方法计数加1,若加1后的值不等于构造方法的值,则线程阻塞。
引申阅读: