一、CountDownLatch:

1、什么是 CountDownLatch:

CountDownLatch,闭锁,就是一个基于 AQS 共享模式的同步计数器,它内部的方法都是围绕 AQS 实现的。主要作用是使一个或一组线程在其他线程执行完毕之前,一直处于等待状态,直到其他线程执行完成后再继续执行。

CountDownLatch 利用 AQS 的 state 变量充当计数器(由 volatile 修饰并使用 CAS 进行更新的),计数器的初始值就是线程的数量,每当一个线程执行完成,计数器的值就会减一,当计数器的值为 0 时,表示所有的线程都已经完成任务了,那么接下来就唤醒在 CountDownLatch 上等待的线程执行后面的任务。

那么当计数器的值为 0 时,主线程是如何被唤醒的呢?这就要从 CountDownLatch 的工作流程来说明了,CountDownLatch 的工作流程可以看成在一开始只在 CLH 队列中放入一个主线程,然后不停的唤醒,唤醒之后如果发现 state 还是不为0,则继续等待。而主线程什么时候会被唤醒呢?当每个子线程执行完毕的时候,会调用 countDown() 并基于 CAS 将计数器 state 的值减一,减一成功释放资源后,就会调用 unparkSuccessor() 唤醒主线程,当所有的子线程都执行完了,也就是 state 为 0 时,这时候主线程被唤醒之后就可以继续执行了。

state 被减成了 0 之后,就无法继续使用这个 CountDownLatch 了,需要重新 new 一个,因为 state 的数量只有在初始化 CountDownLatch 的时候才可以设置,这也是 CountDownLatch 不可重用的原因。

2、CountDownLatch 的源码简单说明:

从代码层面上来看,CountDownLatch 基于内部类 Sync 实现,而 Sync 继承自 AQS。CountDownLatch 最主要有两个方法:await() 和 countDown()

  • await():调用该方法的线程会被挂起,直到 CountDownLatch 计数器的值为 0 才继续执行,底层使用的是 AQS 的 tryAcquireShared()

  • countDown():用于减少计数器的数量,如果计数减为 0 的话,就会唤醒主线程,底层使用的是 AQS 的 releaseShared()

countDown() 方法详细流程:

参考文章:CountDownLatch 原理与应用:https://juejin.cn/post/6900368854472458248

二、CyclicBarrier:

1、什么是CyclicBarrier:

CyclicBarrier,循环栅栏,通过 CyclicBarrier 可以实现一组线程之间的相互等待,当所有线程都到达屏障点之后再执行后续的操作。通过 await() 方法可以实现等待,当最后一个线程执行完,会使得所有在相应 CyclicBarrier 实例上的等待的线程被唤醒,而最后一个线程自身不会被暂停。

CyclicBarrier 没有像 CountDownLatch 和 ReentrantLock 使用 AQS 的 state 变量,它是直接借助 ReentrantLock 加上 Condition 等待唤醒的功能进而实现的。在构建 CyclicBarrier 的时候,传入的值会赋值给 CyclicBarrier 内部维护的变量 count,同时也会赋值给 parties 变量(这是可以复用的关键)。

线程调用 await() 表示线程已经到达栅栏,每次调用 await() 时,会将 count 减一,操作 count 值是直接使用 ReentrantLock 来保证线程安全性的,如果 count 不为 0,则添加到 condition 队列中,如果 count 等于 0,则把节点从 condition 队列中移除并添加到 AQS 队列中进行全部唤醒,并且将 parties 的值重新赋值给 count 从而实现复用。

2、CyclicBarrier 的源码分析:

(1)成员变量:

//同步操作锁
private final ReentrantLock lock = new ReentrantLock();
//线程拦截器
private final Condition trip = lock.newCondition();
//每次拦截的线程数
private final int parties;
//换代前执行的任务
private final Runnable barrierCommand;
//表示栅栏的当前代
private Generation generation = new Generation();
//计数器
private int count;//静态内部类Generation
private static class Generation {boolean broken = false;
}

CyclicBarrier 是通过独占锁实现的,底层包含了 “ReentrantLock 对象 lock” 和 “Condition 对象 trip”,通过条件队列 trip 来对线程进行阻塞的,并且其内部维护了两个 int 型的变量 parties 和 count:

  • parties 表示每次拦截的线程数,该值在构造时进行赋值,用于实现 CyclicBarrier 的复用;

  • count 是内部计数器,它的初始值和 parties 相同,以后随着每次 await 方法的调用而减 1,直到减为 0 就将所有线程唤醒。

CyclicBarrier 有一个静态内部类 Generation,该类的对象代表栅栏的当前代,利用它可以实现循环等待,当 count 减为 0 会将所有阻塞的线程唤醒,并设置成下一代。

barrierCommand 表示换代前执行的任务,在唤醒所有线程前可以通过 barrierCommand 来执行指定的任务

(2)await() 方法:

CyclicBarrier 类最主要的功能就是使先到达屏障点的线程阻塞并等待后面的线程,其中它提供了两种等待的方法,分别是定时等待和非定时等待。

//非定时等待
public int await() throws InterruptedException, BrokenBarrierException {try {return dowait(false, 0L);} catch (TimeoutException toe) {throw new Error(toe);}
}//定时等待
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException {return dowait(true, unit.toNanos(timeout));
}

BrokenBarrierException 表示栅栏已经被破坏,破坏的原因可能是其中一个线程 await() 时被中断或者超时。

可以看到不管是定时等待还是非定时等待,它们都调用了 dowait() 方法,只不过是传入的参数不同而已,下面我们就来看看 dowait() 方法都做了些什么。

//核心等待方法
private int dowait(boolean timed, long nanos) throws InterruptedException, BrokenBarrierException, TimeoutException {// 显示锁final ReentrantLock lock = this.lock;lock.lock();try {final Generation g = generation;//检查当前栅栏是否被打翻if (g.broken) {throw new BrokenBarrierException();}//检查当前线程是否被中断if (Thread.interrupted()) {//如果当前线程被中断会做以下三件事//1.打翻当前栅栏//2.唤醒拦截的所有线程//3.抛出中断异常breakBarrier();throw new InterruptedException();}//每次都将计数器的值减1int index = --count;//计数器的值减为0则需唤醒所有线程并转换到下一代if (index == 0) {boolean ranAction = false;try {//唤醒所有线程前先执行指定的任务final Runnable command = barrierCommand;if (command != null) {command.run();}ranAction = true;//唤醒所有线程并转到下一代nextGeneration();return 0;} finally {//确保在任务未成功执行时能将所有线程唤醒if (!ranAction) {breakBarrier();}}}//如果计数器不为0则执行此循环for (;;) {try {//根据传入的参数来决定是定时等待还是非定时等待if (!timed) {trip.await();}else if (nanos > 0L) {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();}//如果线程因为换代操作而被唤醒则返回计数器的值if (g != generation) {return index;}//如果线程因为时间到了而被唤醒则打翻栅栏并抛出异常if (timed && nanos <= 0L) {breakBarrier();throw new TimeoutException();}}} finally {lock.unlock();}
}

上面执行的代码相对比较容易看懂,我们再来看一下执行流程:

  • 执行 dowait() 方法时,先获得显示锁,判断当前线程状态是否被中断,如果是,则执行 breakBarrier() 方法,唤醒之前阻塞的所有线程,并将计数器重置,否则,往下执行;

  • 计数器 count 减 1,如果 count == 0,表示最后一个线程达到栅栏,接着执行之前指定的 Runnable 接口,同时执行 nextGeneration() 方法进入下一代;

  • 否则,进入自旋,判断当前线程是进入定时等待还是非定时等待,如果在等待过程中被中断,执行 breakBarrier() 方法,唤醒之前阻塞的所有线程;

  • 判断是否是因为执行 breakBarrier() 方法而被唤醒,如果是,则抛出异常;

  • 判断是否是正常的换代操作而被唤醒,如果是,则返回计数器的值;

  • 判断是否是超时而被唤醒,如果是,则唤醒之前阻塞的所有线程,并抛出异常;

  • 释放锁。

(3)breakBarrier() 方法:

private void breakBarrier() {generation.broken = true;//栅栏被打破count = parties;//重置counttrip.signalAll();//唤醒之前阻塞的线程
}

(4)nextGeneration() 方法:

private void nextGeneration() {//唤醒所以的线程trip.signalAll();//重置计数器count = parties;//重新开始generation = new Generation();
}

(5)reset()方法:

// 重置barrier到初始状态,所有还在等待中的线程最终会抛出BrokenBarrierException。
public void reset() {final ReentrantLock lock = this.lock;lock.lock();try {breakBarrier();   // break the current generationnextGeneration(); // start a new generation} finally {lock.unlock();}
}

参考文章:CyclicBarrier 原理与使用:https://juejin.cn/post/6900548129645395982

三、Semaphore:

1、什么是 Semaphore:

Semaphore 信号量,主要用于控制并发访问共享资源的线程数量,底层基于 AQS 共享模式,并依赖 AQS 的变量 state 作为许可证 permit,通过控制许可证的数量,来保证线程之间的配合。线程使用 acquire() 获取访问许可,只有拿到 “许可证” 后才能继续运行,当 Semaphore 的 permit 不为 0 的时候,对请求资源的线程放行,同时 permit 的值减1,当 permit 的值为 0 时,那么请求资源的线程会被阻塞直到其他线程释放访问许可,当线程对共享资源操作完成后,使用 release() 归还访问许可。不同于 CyclicBarrier 和 ReentrantLock,Semaphore 不会使用到 AQS 的 Condition 条件队列,都是在 CLH 同步队列中操作,只是当前线程会被 park。另外 Semaphore 是不可重入的。

2、Semaphore 的公平和非公平两种模式:

Semaphore 通过自定义两种不同的同步器(FairSync 和 NonfairSync)提供了公平和非公平两种工作模式,两种模式下分别提供了限时/不限时、响应中断/不响应中断的获取资源的方法(限时获取总是及时响应中断的),而所有的释放资源的 release() 操作是统一的。

  • 公平模式:遵循 FIFO,调用 acquire() 方法获取许可证的顺序时,先判断同步队列中是不是存在其他的等待线程,如果存在就将请求线程封装成 Node 结点加入同步队列,从而保证每个线程获取同步状态都是按照先到先得的顺序执行的,否则对 state 值进行减操作并返回剩下的信号量

  • 非公平模式:是抢占式的,通过竞争的方式获取,不管同步队列中是否存在等待线程,有可能一个新的获取线程恰好在一个许可证释放时得到了这个许可证,而前面还有等待的线程。

框架流程图如下:

3、尝试获取资源 acquire()方法的执行流程图:

参考文章:Semaphore 源码解析:https://juejin.cn/post/6899744477427007495

文章推荐:CountDownLatch和CyclicBarrier的区别:https://juejin.cn/post/6989419875366076447

JUC多线程:CountDownLatch、CyclicBarrier、Semaphore同步器原理总结相关推荐

  1. JUC多线程:synchronized锁机制原理 与 Lock锁机制

    前言: 线程安全是并发编程中的重要关注点,造成线程安全问题的主要原因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据.因此为了解决这个问题,我们可能需要这样一个方案,当存在多 ...

  2. JUC介绍--常用辅助类(CountDownLatch CyclicBarrier Semaphore)

    CountDownLatch减法计数器 每次有线程调用countDownLatch.countDown()数量就-1 数量减到0时,countDownLatch.await()会被唤醒,继续向下执行 ...

  3. (面经总结)一篇文章带你完整复习 Java 中并发关键字(CountDownLatch/CyclicBarrier/Semaphore/Volatile)

    文章目录 一.倒计数器:CountDownLatch 二.循环栅栏:CyclicBarrier 三.信号量:Semaphore 四.volatile 关键字的作用 一.倒计数器:CountDownLa ...

  4. CountDownLatch CyclicBarrier Semaphore

    CountDownLacth CountDownLacth(倒计数锁存器)到底有什么用呢?我们来看下面这个场景. 我们又有小片片要开始拍摄了,这个小片片需要5个演员来演,开演之前,导演需要这5个演员全 ...

  5. 尚硅谷-互联网大厂高频重点面试题 (第2季)JUC多线程及高并发

    本期内容包括 JUC多线程并发.JVM和GC等目前大厂笔试中会考.面试中会问.工作中会用的高频难点知识. 斩offer.拿高薪.跳槽神器,对标阿里P6的<尚硅谷_互联网大厂高频重点面试题(第2季 ...

  6. JAVA社交平台项目第七天 JUC多线程

    JUC多线程 (一) 学习目标: 掌握多线程的创建 掌握线程安全的处理 了解线程状态 掌握线程停止的两种方法 了解线程的原子性,可见性和有序性 理解内存可见性的原理 掌握synchronized解决内 ...

  7. AQS抽象队列同步器原理详解

    系列文章目录 第一节 synchronized关键字详解-偏向锁.轻量级锁.偏向锁.重量级锁.自旋.锁粗化.锁消除 AQS抽象队列同步器原理详解 系列文章目录 前言 一.AQS特性 二.AQS原理 1 ...

  8. CyclicBarrier 使用核心原理 图解

    疯狂创客圈 经典图书 : <Netty Zookeeper Redis 高并发实战> 面试必备 + 面试必备 + 面试必备 [博客园总入口 ] 疯狂创客圈 经典图书 : <Sprin ...

  9. Java 多线程 —— 深入理解 volatile 的原理以及应用

    转载自  Java 多线程 -- 深入理解 volatile 的原理以及应用 推荐阅读:<java 多线程-线程怎么来的> 这一篇主要讲解一下volatile的原理以及应用,想必看完这一篇 ...

最新文章

  1. 利用Python对销售额进行预测
  2. Netty堆外内存泄露排查与总结 1
  3. 数据通路习题分析之二
  4. VS的一个项目,release/debug/x64/win32的设置有没有办法一次设置?
  5. 全国哀悼日,网站变成黑白色
  6. 英语学习软件——《经典双语广告语大全》(图)
  7. 电商业务Alipay支付实战(当面付实现)
  8. SIMPLE、PISO 、PIMPLE算法浅析
  9. 微信授权登录(微信订阅号使用测试账号)
  10. IOS目标检测(翻译)
  11. nohup ——Linux后台运行命令
  12. 业务连续性方案概述【9】
  13. 【功能安全】【ISO26262】支持过程
  14. 海康、大华、华为等GB28181国标平台向上级联给LiveGBS GB28181平台的操作示例
  15. C++内存分布探讨,x86和x64位的细微区别
  16. 大学生没有项目经验该怎么拿测开岗位的office?来看话梅怎么说
  17. Go 使用for range time.Tick() 设置定时器
  18. Mogafx日元展望
  19. Sanic框架下部署Pytorch模型
  20. javascript eq()用法

热门文章

  1. 【小白学习keras教程】一、基于波士顿住房数据集训练简单的MLP回归模型
  2. 五十六、TodoList的三种写法,祭奠我的前端之路
  3. 八、爬虫解析利器 PyQuery 的使用
  4. 机器学习中的逻辑回归
  5. 人工智能时代背景下,NLP方向或将悄悄崛起
  6. 边界化难题终结者!将自监督学习应用到自动驾驶上 | CVPR 2021
  7. 实用教程:如何制作学术会议Oral/Spotlight Video?
  8. web第五章 json
  9. waitpid最后以一个参数设为0_西门子MM440变频器参数设置
  10. 【文件格式问题】文件格式 Windows、Unix/Linux、Mac 导致的问题及处理(idea 或 notepad++ 档案格式转换方法)