关于作者: CSDN内容合伙人、技术专家, 从零开始做日活千万级APP,带领团队单日营收超千万。
专注于分享各领域原创系列文章 ,擅长java后端、移动开发、商业化变现、人工智能等,希望大家多多支持。
目录
- 一、导读
- 二、概览
- CyclicBarrier 和 countdownlatch 的区别
- 三、使用
- 四、原理
- 五、 推荐阅读
一、导读
我们继续总结学习Java基础知识,温故知新。
本文涉及知识点:
AQS - AbstractQueuedSynchronizer
CAS(Compare And Swap)
锁概念 volatile
ReentrantLock
二、概览
一、是什么
CyclicBarrier是JDK提供的一个同步工具,它的作用是让一组线程全部达到一个状态之后再全部同时执行。
特点:
所有线程执行完毕之后是可以重用的。
二、实现原理
CyclicBarrier就是利用了 AQS - AbstractQueuedSynchronizer 的共享锁来实现。
调用await的线程会被阻塞,直到计数器为0,在继续执行。
内部维护parties记录总线程数,count用于计数,最开始count=parties,调用await()之后count原子递减,当count为0之后,再次将parties赋值给count,这就是复用的原理
三、使用场景
一组线程全部达到一个状态之后再全部同时执行。
eg:
多线程计算结果,最后在合并结果。
四、优劣
- 优点 :代码优雅,不需要对线程池进行操作,将线程池作为 Bean 的情况下有很好的使用场景。
- 缺点 :需要提前知道线程数量;性能确实,呃呃呃呃呃,差了点。哦对了,还需要在线程代码块内加上异常判断,否则在 countDown 之前发生异常而没有处理,就会导致主线程永远阻塞在 await。
需要提前知道数量。
需要注意异常的处理,await() 跟 num 必须成对出现,否则线程会一直阻塞。
CyclicBarrier 和 countdownlatch 的区别
CyclicBarrier和CountDownLatch都是多线程同步工具,但是它们的工作机制和用途略有不同:
CyclicBarrier更适合于需要循环等待多个线程的场景,而CountDownLatch则更适合于需要等待特定数量线程完成的场景。
-
构造和计数机制
CountDownLatch在构造时需要一个初始计数值,每调用一次countDown()方法,计数器减1,当计数器达到0时,await()方法会释放等待线程。CyclicBarrier则不需要初始计数值,它通过在每个线程中设置一个屏障(barrier)来阻塞线程,直到所有线程都到达屏障后才会释放。 -
线程状态
CountDownLatch不会改变线程状态,只是让线程等待,而CyclicBarrier在屏障未达到时阻塞线程,达到屏障后才解除阻塞。 -
重用性
CyclicBarrier是可重用的,在所有线程都通过屏障后,可以再次设置新的屏障,而CountDownLatch是一次性的。 -
等待线程数
CountDownLatch等待的线程数是固定的,而CyclicBarrier可以等待任意数量的线程
countdownlatch 是一个线程等待其他线程执行完毕后再执行,
CyclicBarrier 是每一个线程等待所有线程执行完毕后,再执行
三、使用
从字面上的意思可以知道,这个类的中文意思是“循环栅栏”。大概的意思就是一个可循环利用的屏障。
它的作用就是会让所有线程都等待完成后才会继续下一步行动。
现实生活中我们经常会遇到这样的情景,在进行某个活动前需要等待人全部都齐了才开始。例如吃饭时要等全家人都上座了才动筷子,旅游时要等全部人都到齐了才出发,比赛时要等运动员都上场后才开始。
CyclicBarrier叫做回环屏障,它的作用是让一组线程全部达到一个状态之后再全部同时执行,而且他有一个特点就是所有线程执行完毕之后是可以重用的。
public class CyclicBarrierTest {private static int num = 3;private static CyclicBarrier cyclicBarrier = new CyclicBarrier(num, () -> {System.out.println("---------所有人都好了, await 次数到 ----------");});private static ExecutorService executorService = Executors.newFixedThreadPool(num);public static void main(String[] args) throws Exception{test2Cyclic();
// test2Cyclic1();executorService.shutdown();}public static void test2Cyclic1() {executorService.submit(() -> {System.out.println("A在上厕所");try {Thread.sleep(4000);System.out.println("A上完了");cyclicBarrier.await();System.out.println("会议结束,A退出");} catch (Exception e) {e.printStackTrace();}finally {}});executorService.submit(()->{System.out.println("B在上厕所");try {Thread.sleep(2000);System.out.println("B上完了");cyclicBarrier.await();System.out.println("会议结束,B退出");} catch (Exception e) {e.printStackTrace();}finally {}});executorService.submit(()->{System.out.println("C在上厕所");try {Thread.sleep(3000);System.out.println("C上完了");cyclicBarrier.await();System.out.println("会议结束,C退出");} catch (Exception e) {e.printStackTrace();}finally {}});}public static void test2Cyclic() {executorService.submit(() -> {System.out.println("A在上厕所");try {Thread.sleep(4000);System.out.println("A上完了");cyclicBarrier.await();System.out.println("会议结束,A退出,开始撸代码");cyclicBarrier.await();System.out.println("C工作结束,下班回家");cyclicBarrier.await();} catch (Exception e) {e.printStackTrace();} finally {}});executorService.submit(() -> {System.out.println("B在上厕所");try {Thread.sleep(2000);System.out.println("B上完了");cyclicBarrier.await();System.out.println("会议结束,B退出,开始摸鱼");cyclicBarrier.await();System.out.println("B摸鱼结束,下班回家");cyclicBarrier.await();} catch (Exception e) {e.printStackTrace();} finally {}});executorService.submit(() -> {System.out.println("C在上厕所");try {Thread.sleep(3000);System.out.println("C上完了");cyclicBarrier.await();System.out.println("会议结束,C退出,开始摸鱼");cyclicBarrier.await();System.out.println("C摸鱼结束,下班回家");cyclicBarrier.await();} catch (Exception e) {e.printStackTrace();} finally {}});executorService.shutdown();}
}
四、原理
子类的任务有:
- 通过CAS操作维护共享变量state。
- 重写资源的获取方式。
- 重写资源释放的方式。
一起看下代码
// 1、 创建CountDownLatch并设置计数器值,count代表计数器个数(内部是共享锁,本质就是上了几次锁)
第二个构造方法有一个 Runnable 参数,这个参数的意思是最后一个到达线程要做的任务
public CyclicBarrier(int parties, Runnable barrierAction) // 2、子线程调用await()方法时,获取独占锁,同时对count递减,进入阻塞队列,然后释放锁
// 当第一个线程被阻塞同时释放锁之后,其他子线程竞争获取锁,操作同1
public void await()// 等待一定时间
public void await(long timeout, TimeUnit unit)//同步锁private final ReentrantLock lock = new ReentrantLock();//条件队列private final Condition trip = lock.newCondition();
CyclicBarrier借助ReentranLock与Condition来对线程进行阻塞的
private int dowait(boolean timed, long nanos)throws InterruptedException, BrokenBarrierException,TimeoutException {final ReentrantLock lock = this.lock;lock.lock();try {//当前代final Generation g = generation;//判断当前代的状态,如果当前代后的屏障被打破,则g.broken返回true,否则返回false。if (g.broken)throw new BrokenBarrierException();//判断当前线程是否被中断if (Thread.interrupted()) {//如果当前线程已经被中断,则调用breakBarrier()//该方法代码为generation.broken = true;count = parties;trip.signalAll();//可见,只做了3件事:先将当前代的屏障变为打破状态,接着重置计数器的值,最后唤醒所有被阻塞的线程breakBarrier();//最后抛出中断异常throw new InterruptedException();}//将计数器的值减1int index = --count;if (index == 0) {//如果当前计数器的值为0boolean ranAction = false;try {//则先执行换代任务,可以看得出来,是由最后一个到达屏障的线程执行的final Runnable command = barrierCommand;if (command != null)command.run();ranAction = true;//开启下一代,这个方法的代码为trip.signalAll();count = parties;generation = new Generation();//该代码唤醒所有被阻塞的线程,重置计数器的值,并且实例化下一代nextGeneration();return 0;} finally {//如果换代任务未执行成功,则先将当前代的屏障变为打破状态,接着重置计数器的值,最后唤醒所有被阻塞的线程if (!ranAction)breakBarrier();}}//当前线程一直阻塞,直到“有parties个线程到达barrier” 或 “当前线程被中断” 或 “超时”这3者之一发生//死循环for (;;) {try {if (!timed)//如果不是定时等待,则调用条件队列的await()进行阻塞trip.await();else if (nanos > 0L)//如果是定时等待,则调用条件队列的awaitNanos进行等待nanos = trip.awaitNanos(nanos);} catch (InterruptedException ie) {//如果在等待过程中,当前线程被打断if (g == generation && ! g.broken) {//被打断后,还处于当前代,且当前代的屏障也未被打破//现在的情况是,最后一个线程还未到屏障,当前线程早早到了,并且在进行等待,但是在等待的过程中,被打断了。//则打破当前代的屏障,唤醒所有被阻塞的线程breakBarrier();throw ie;} else {//如果已经换代,则手动进行打断Thread.currentThread().interrupt();}}//此时线程被唤醒,需要判断自己为什么被唤醒了 //如果是其他某个线程被打断或者是由于超时导致当前代的屏障被打破,则抛出异常if (g.broken)throw new BrokenBarrierException();//如果是正常换代,则返回index值if (g != generation)return index;//如果是定时等待,且时间已经到了,则打破屏障,唤醒所有阻塞的线程,最后抛出异常if (timed && nanos <= 0L) {breakBarrier();throw new TimeoutException();}}} finally {lock.unlock();}}
- 当子线程调用await()方法时,获取独占锁,同时对count递减,进入阻塞队列,然后释放锁
- 当第一个线程被阻塞同时释放锁之后,其他子线程竞争获取锁,操作同1
- 直到最后count为0,执行CyclicBarrier构造函数中的任务,执行完毕之后子线程继续向下执行
五、 推荐阅读
Java 专栏
SQL 专栏
数据结构与算法
Android学习专栏