一、背景
大家平时应该也遇到过这样的场景,使用多线程执行一段操作,然后依赖这一段操作的结果再执行其他逻辑。这个时候我们就要控制线程之间的顺序,必须保证该多线程操作执行完之后才开始执行后面的逻辑。
那么今天这篇文章将介绍CountDownLatch和CyclicBarrier的用法以及如何使用它们分别来实现以上场景。
二、CountDownLatch用法
概念:
CountDownLatch:具有计数器的功能,等待其他线程执行完毕,主线程在继续执行,用于监听某些初始化操作,并且线程进行阻塞,等初始化执行完毕后,通知主线程继续工作执行。
值得注意的是CountDownLatch计数的次数一定要与构造器传入的数字一致,比如构造器传入的是3,则countDown()一定要执行3次,否则线程将一直阻塞。CountDownLatch通常用来控制线程等待,它可以让线程等待倒计时结束,再开始执行。
特点:
只能一次性使用(不能reset);主线程阻塞;某个线程中断将永远到不了屏障点,所有线程都会一直等待。
CountDownLatch类只提供了一个构造器:
public CountDownLatch(int count) { }; //参数count为计数值
下面这3个方法是CountDownLatch类中最重要的方法:
public void await() throws InterruptedException { }; //调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行 public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; //和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行 public void countDown() { }; //将count值减1
构造器中的计数值(count)实际上就是闭锁需要等待的线程数量。这个值只能被设置一次,而且CountDownLatch没有提供任何机制去重新设置这个计数值。与CountDownLatch的第一次交互是主线程等待其他线程。主线程必须在启动其他线程后立即调用CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。其他N 个线程必须引用闭锁对象,因为他们需要通知CountDownLatch对象,他们已经完成了各自的任务。这种通知机制是通过 CountDownLatch.countDown()方法来完成的;每调用一次这个方法,在构造函数中初始化的count值就减1。所以当N个线程都调用了这个方法,count的值等于0,然后主线程就能通过await()方法,恢复执行自己的任务。
用法:
我们描述这样一个场景:三位运动员比赛跑步,当三位运动员都准备好之后比赛才开始。
代码实现如下
import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * <p> * * </p> * * @className ThreadTest * @author Sue * @create 2021/8/27 **/ public class ThreadTestA { //创建初始化3个线程的线程池 private final ExecutorService threadPool = Executors.newFixedThreadPool(3); private final CountDownLatch countDownLatch = new CountDownLatch(3); private void ready() { for (int i = 0; i < 3; i++) { threadPool.execute(() -> { try { //让该线程等待,假设为[0,5000]的随机数 long times = Math.round(Math.random() * 5000); System.out.println("运动员" + Thread.currentThread().getName() + "需要准备" + times + "ms"); Thread.sleep(times); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "运动员准备完毕"); countDownLatch.countDown(); }); } threadPool.shutdown(); } public void run() { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "所有运动员准备完毕!比赛开始"); } public static void main(String[] args) { long now = System.currentTimeMillis(); ThreadTestA threadTestA = new ThreadTestA(); threadTestA.ready(); threadTestA.run(); long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + (end - now)); } }
执行后输出结果,从结果可以看出确实是在三个线程都执行完成之后,才开始执行主线程的run方法。
三、CyclicBarrier用法
概念:
CyclicBarrier翻译过来就是循环屏障的意思,其作用就是让一组线程到达一个同步点后再一起继续运行,在其中任意一个线程未达到同步点,其他到达的线程均会被阻塞。这个屏障之所以用循环修饰,是因为在所有的线程释放彼此之后,这个屏障是可以重新使用的,这一点与CountDownLatch不同。假设有一个场景:每个线程代表一个跑步运动员,当运动员都准备好后,才一起出发,只要有一个人没有准备好,大家都等待。
CyclicBarrier是一种同步机制允许一组线程相互等待,等到所有线程都到达一个屏障点才退出await方法,它没有直接实现AQS而是借助ReentrantLock来实现的同步机制。它是可循环使用的,而CountDownLatch是一次性的,另外它体现的语义也跟CountDownLatch不同,CountDownLatch减少计数到达条件采用的是release方式,而CyclicBarrier走向屏障点(await)采用的是Acquire方式,Acquire是会阻塞的,这也实现了CyclicBarrier的另外一个特点,只要有一个线程中断那么屏障点就被打破,所有线程都将被唤醒(CyclicBarrier自己负责这部分实现,不是由AQS调度的),这样也避免了因为一个线程中断引起永远不能到达屏障点而导致其他线程一直等待。屏障点被打破的CyclicBarrier将不可再使用(会抛出BrokenBarrierException)除非执行reset操作。
CyclicBarrier类位于java.util.concurrent包下,CyclicBarrier提供2个构造器:
public CyclicBarrier(int parties, Runnable barrierAction) {}
public CyclicBarrier(int parties) {}
参数parties指让多少个线程或者任务等待至barrier状态;参数barrierAction为当这些线程都达到barrier状态时会执行的内容。
CyclicBarrier中最重要的方法就是await方法,它有2个重载版本:
public int await() throws InterruptedException, BrokenBarrierException { };
public int await(long timeout, TimeUnit unit)throws InterruptedException,BrokenBarrierException,TimeoutException { };
第一个版本比较常用,用来挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务;
第二个版本是让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务。
CyclicBarrier常用方法说明
getParties()
获取CyclicBarrier打开屏障的线程数量。
getNumberWaiting()
获取正在CyclicBarrier上等待的线程数量。
await()
在CyclicBarrier上进行阻塞等待,直到发生以下情形之一:
- 在CyclicBarrier上等待的线程数量达到parties,则所有线程被释放,继续执行。
- 当前线程被中断,则抛出InterruptedException异常,并停止等待,继续执行。
- 其他等待的线程被中断,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
- 其他等待的线程超时,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
- 其他线程调用CyclicBarrier.reset()方法,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
await(timeout,TimeUnit)
在CyclicBarrier上进行限时的阻塞等待,直到发生以下情形之一:
- 在CyclicBarrier上等待的线程数量达到parties,则所有线程被释放,继续执行。
- 当前线程被中断,则抛出InterruptedException异常,并停止等待,继续执行。
- 当前线程等待超时,则抛出TimeoutException异常,并停止等待,继续执行。
- 其他等待的线程被中断,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
- 其他等待的线程超时,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
- 其他线程调用CyclicBarrier.reset()方法,则当前线程抛出BrokenBarrierException异常,并停止等待,继续执行。
isBroken()
获取是否破损标志位broken的值,此值有以下几种情况:
- CyclicBarrier初始化时,broken=false,表示屏障未破损。
- 如果正在等待的线程被中断,则broken=true,表示屏障破损。
- 如果正在等待的线程超时,则broken=true,表示屏障破损。
- 如果有线程调用CyclicBarrier.reset()方法,则broken=false,表示屏障回到未破损状态。
reset()
使得CyclicBarrier回归初始状态,直观来看它做了两件事:
- 如果有正在等待的线程,则会抛出BrokenBarrierException异常,且这些线程停止等待,继续执行。
- 将是否破损标志位broken置为false。
用法:我们继续使用上面的例子,但是使用CyclicBarrier来实现
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * <p> * * </p> * * @className Test03 * @author Sue * @create 2021/8/27 **/ public class ThreadTestB implements Runnable { private static final int THREAD_COUNT_NUM = 3; //创建初始化3个线程的线程池 private final ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT_NUM); //创建3个CyclicBarrier对象,执行完后执行当前类的run方法 private CyclicBarrier cb = new CyclicBarrier(THREAD_COUNT_NUM, this); private void ready() { for (int i = 0; i < THREAD_COUNT_NUM; i++) { threadPool.execute(() -> { //让该线程等待,假设为[0,5000]的随机数 long times = Math.round(Math.random() * 5000); try { Thread.sleep(times); System.out.println("运动员" + Thread.currentThread().getName() + "正在准备,用时" + times + "ms"); } catch (InterruptedException e) { e.printStackTrace(); } try { //执行完运行await(),等待所有运动员准备完毕 cb.await(); System.out.println("运动员" + Thread.currentThread().getName() + "已出发!"); } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } }); } } @Override public void run() { System.out.println(Thread.currentThread().getName() + "所有运动员准备完毕!比赛开始"); threadPool.shutdown(); } public static void main(String[] args) { long now = System.currentTimeMillis(); ThreadTestB cb = new ThreadTestB(); cb.ready(); long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + (end - now)); } }
执行后输出结果
同样可以看出,只有在最后一个线程达到屏障之后,才会从三个线程中选择一个线程去执行Runnable,且不会阻塞主线程。
一个屏障可以多次使用的,代码如下。
import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * <p> * * </p> * * @className Test03 * @author Sue * @create 2021/8/27 **/ public class ThreadTestC { //线程数量 private static final int THREAD_COUNT_NUM = 3; //创建初始化3个线程的线程池 private final ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT_NUM); //创建3个CyclicBarrier对象 private final CyclicBarrier cb1 = new CyclicBarrier(THREAD_COUNT_NUM, () -> { System.out.println("所有运动员已入场!开始准备比赛"); threadPool.shutdown(); }); private void entrance() { for (int i = 0; i < THREAD_COUNT_NUM; i++) { threadPool.execute(() -> { try { //让该线程等待,假设为[0,1000]的随机数 long times = Math.round(Math.random() * 5000); Thread.sleep(times); System.out.println("运动员" + Thread.currentThread().getName() + "已入场,用时" + times + "ms"); } catch (InterruptedException e) { e.printStackTrace(); } try { //执行完运行await(),等待所有运动员入场 cb1.await(); } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } }); } } private void ready() { for (int i = 0; i < THREAD_COUNT_NUM; i++) { threadPool.execute(() -> { try { //让该线程等待,假设为[0,5000]的随机数 long times = Math.round(Math.random() * 5000); Thread.sleep(times); System.out.println("运动员" + Thread.currentThread().getName() + "准备完毕,用时" + times + "ms"); } catch (InterruptedException e) { e.printStackTrace(); } try { //执行完运行await(),等待所有运动员准备完毕 cb1.await(); } catch (InterruptedException | BrokenBarrierException e) { e.printStackTrace(); } }); } } public static void main(String[] args) { long now = System.currentTimeMillis(); ThreadTestC cb = new ThreadTestC(); //入场 cb.entrance(); //准备 cb.ready(); long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + (end - now)); } }
从执行结果可以看出,在初次的4个线程越过barrier状态后,又可以用来进行新一轮的使用。而CountDownLatch无法进行重复使用。
总结
通过上面的几个例子,想必应该对CountDownLatch和CyclicBarrier有一些了解了。我们再来总结一下两者的区别。
- CountDownLatch和CyclicBarrier都能够实现线程之间的等待,只不过它们侧重点不同:
- CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;
- 而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;
- 另外,CountDownLatch是不能够重用的,而CyclicBarrier是可以重用的。
- CountDownLatch会阻塞主线程,CyclicBarrier不会阻塞主线程,只会阻塞子线程。
- CyclicBarrier可以使用reset()方法重置屏障点
如果想了解更多的用法,可以参考以下链接
Java并发编程:CountDownLatch、CyclicBarrier和Semaphore