当前位置: 代码迷 >> 综合 >> CountDownLatch-CyclicBarrier-Semaphore解析
  详细解决方案

CountDownLatch-CyclicBarrier-Semaphore解析

热度:46   发布时间:2024-01-17 07:17:39.0
一、前言

CountDownLatch、CyclicBarrier、Semaphore是JUC包下常用的同步锁,其中CountDownLatch、Semaphore是基于AQS(AbstractQueuedSynchronizer)实现的共享锁。下面来看下这三个锁的具体区别。

二、正文

1.CountDownLatch解析

先看下官方源码解释:

A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.

意思是,它是一个同步辅助器,允许一个或多个线程一直等待,直到一组在其他线程执行的操作全部完成。

Tip:CountDownLatch : 一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行,重点是一个线程(或者多个)等待多个线程

再来看下其构造函数:

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");this.sync = new Sync(count);
}

它的构造方法,会传入一个 count 值,用于计数。

常用的方法有两个:

// 调用await()方法的线程会被挂起,它会等待直到count值为0才继续执行
public void await() throws InterruptedException {
     };  
// 和await()类似,只不过等待一定的时间后count值还没变为0的话就会继续执行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException {
     };  
//将count值减1
public void countDown() {
     };  

当一个线程调用await方法时,就会阻塞当前线程。每当有线程调用一次 countDown 方法时,计数就会减 1。当 count 的值等于 0 的时候,被阻塞的线程才会继续运行。

我们用一个栗子来说明下,应该会更好的理解:

现在设想一个场景,公司项目,线上出现了一个紧急 bug,被客户投诉,领导焦急的过来,想找人迅速的解决这个 bug 。

那么,一个人解决肯定速度慢啊,于是叫来张三和李四,一起分工解决。终于,当他们两个都做完了自己所需要做的任务之后,领导才可以答复客户,客户也就消气了。

于是,我们可以设计一个 Worker 类来模拟单个人修复 bug 的过程,主线程就是领导,一直等待所有 Worker 任务执行结束,主线程才可以继续往下走。

public class CountDownTest {
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static void main(String[] args) throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(2);ExecutorService executorService = Executors.newCachedThreadPool();executorService.execute(new Worker("张三", 2000, latch));executorService.execute(new Worker("李四", 3000, latch));long startTime = System.currentTimeMillis();latch.await();System.out.println("bug全部解决,领导可以给客户交差了,任务总耗时:"+ (System.currentTimeMillis() - startTime));executorService.shutdown();}static class Worker extends Thread{
    String name;int workTime;CountDownLatch latch;public Worker(String name, int workTime, CountDownLatch latch) {
    this.name = name;this.workTime = workTime;this.latch = latch;}@Overridepublic void run() {
    System.out.println(name+"开始修复bug,当前时间:"+sdf.format(new Date()));doWork();System.out.println(name+"结束修复bug,当前时间:"+sdf.format(new Date()));latch.countDown();}private void doWork() {
    try {
    //模拟工作耗时Thread.sleep(workTime);} catch (InterruptedException e) {
    e.printStackTrace();}}}
}

输入结果如下:

张三开始修复bug,当前时间:2021-05-24 14:41:25
李四开始修复bug,当前时间:2021-05-24 14:41:25
张三结束修复bug,当前时间:2021-05-24 14:41:27
李四结束修复bug,当前时间:2021-05-24 14:41:28
bug全部解决,领导可以给客户交差了,任务总耗时:3012

看,CountDownLatch主要的思想是"领导要等待程序员同学们把bug都修改完了,才能给客户交代",即一个线程等待多个线程。

2.CyclicBarrier解析

我也先来看下官方源码解释:

A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point.The barrier is called cyclic because it can be re-used after the waiting threads are released.

意思是:一组线程会互相等待,直到所有线程都到达一个同步点。这个就非常有意思了,就像一群人被困到了一个栅栏前面,只有等最后一个人到达之后,他们才可以合力把栅栏(屏障)突破。

Tip:重点是N个线程,他们之间任何一个没有完成,所有的线程都必须等待。你仔细品下和CountDownLatch的区别。

CyclicBarrier提供了两种构造函数:

public CyclicBarrier(int parties) {
    this(parties, null);
}
public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();this.parties = parties;this.count = parties;this.barrierCommand = barrierAction;
}

第一个构造的参数,指的是需要几个线程一起到达,才可以使所有线程取消等待。第二个构造,额外指定了一个参数,用于在所有线程达到屏障时,优先执行 barrierAction。

常用的方法只有一个await():

//挂起当前线程,直至所有线程都到达barrier状态再同时执行后续任务
public int await() throws InterruptedException, BrokenBarrierException {
     };
//让这些线程等待至一定的时间,如果还有线程没有到达barrier状态就直接让到达barrier的线程执行后续任务
public int await(long timeout, TimeUnit unit)throws InterruptedException,BrokenBarrierException,TimeoutException {
     };

我们先举个栗子,这样应该能够更好理解:

现在模拟一个常用的场景,一组运动员比赛 1000 米,只有在所有人都准备完成之后,才可以一起开跑。

定义一个 Runner 类代表运动员,其内部维护一个共有的 CyclicBarrier,每个人都有一个准备时间,准备完成之后,会调用 await 方法,等待其他运动员。当所有人准备都 OK 时,就可以开跑了。

public class CyclicBarrierTest {
    public static void main(String[] args) {
    CyclicBarrier barrier = new CyclicBarrier(3, new Runnable() {
    @Overridepublic void run() {
    try {
    System.out.println("等裁判吹口哨...");//这里停顿两秒更便于观察线程执行的先后顺序Thread.sleep(2000);System.out.println("裁判吹口哨->>>>>");} catch (InterruptedException e) {
    e.printStackTrace();}}});Runner runner1 = new Runner(barrier, "张三");Runner runner2 = new Runner(barrier, "李四");Runner runner3 = new Runner(barrier, "王五");ExecutorService service = Executors.newFixedThreadPool(3);service.execute(runner1);service.execute(runner2);service.execute(runner3);service.shutdown();}
}class Runner implements Runnable{
    private CyclicBarrier barrier;private String name;public Runner(CyclicBarrier barrier, String name) {
    this.barrier = barrier;this.name = name;}@Overridepublic void run() {
    try {
    //模拟准备耗时Thread.sleep(new Random().nextInt(5000));System.out.println(name + ":准备OK");barrier.await();System.out.println(name +": 开跑");} catch (InterruptedException e) {
    e.printStackTrace();} catch (BrokenBarrierException e){
    e.printStackTrace();}}
}

输出结果如下:

张三:准备OK
李四:准备OK
王五:准备OK
等裁判吹口哨...
裁判吹口哨->>>>>
王五: 开跑
张三: 开跑
李四: 开跑

刚才,提到了循环利用是怎么体现的呢?我现在把屏障值改为 2,然后增加一个“赵六” 一起参与赛跑。被修改的部分如下:

在这里插入图片描述

输出结果如下:

张三:准备OK
李四:准备OK
等裁判吹口哨...
裁判吹口哨->>>>>
李四: 开跑
张三: 开跑
赵六:准备OK
王五:准备OK
等裁判吹口哨...
裁判吹口哨->>>>>
王五: 开跑
赵六: 开跑

发现没,可以分两批,第一批先跑两个人,然后第二批再跑两个人。也就实现了屏障的循环使用。

3.CountDownLatch和CyclicBarrier区别?

CountDownLatch:

它是一个同步辅助器,允许一个或多个线程一直等待,直到一组在其他线程执行的操作全部完成。

重点:是一个线程(或者多个)等待多个线程。

CyclicBarrier:

一组线程会互相等待,直到所有线程都到达一个同步点。这个就非常有意思了,就像一群人被困到了一个栅栏前面,只有等最后一个人到达之后,他们才可以合力把栅栏(屏障)突破。

重点:是N个线程,他们之间任何一个没有完成,所有的线程都必须等待。

下面来看下它们两个的对比:
在这里插入图片描述

4.Semaphore解析

Semaphore 信号量,用来控制同一时间,资源可被访问的线程数量,一般可用于流量的控制。

Semaphore有两个构造方法:

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

默认的不传入fair参数,使用的非公平锁。

常用的方法有两个:

获取许可:

public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

释放许可:

public void release() {
    sync.releaseShared(1);
}

来,我们在举个栗子

打个比方,现在有一段公路交通比较拥堵,那怎么办呢。此时,就需要警察叔叔出面,限制车的流量。

比如,现在有 10 辆车要通过这个地段, 警察叔叔规定同一时间,最多只能通过 5 辆车,其他车辆只能等待。只有拿到许可的车辆可通过,等车辆通过之后,再归还许可,然后把它发给等待的车辆,获得许可的车辆再通行,依次类推。

public class SemaphoreTest {
    private static int count = 10;public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(count);Semaphore semaphore = new Semaphore(5);Random random = new Random();for (int i = 0; i < count; i++) {
    final int no = i;executorService.execute(new Runnable() {
    @Overridepublic void run() {
    try {
    // 获得许可semaphore.acquire();System.out.println(no + ":号车可通行");// 模拟车辆通行耗时Thread.sleep(random.nextInt(2000));// 释放许可semaphore.release();}catch (InterruptedException e) {
    e.printStackTrace();}}});}executorService.shutdown();}
}

输出结果如下:

0:号车可通行
1:号车可通行
2:号车可通行
3:号车可通行
4:号车可通行-------------------------------------
5:号车可通行
6:号车可通行
7:号车可通行
8:号车可通行
9:号车可通行

中间的虚线是我自己画上去的,表示每次只能通行5量车。

细心的读者,就会发现,这许可一共就发 5 个,那等第一批车辆用完释放之后, 第二批的时候应该发给谁呢?

这确实是一个问题。所有等待的车辆都想先拿到许可,先通行,怎么办。这就需要,用到锁了。就所有人都去抢,谁先抢到,谁就先走呗。

我们去看一下 Semaphore的构造函数,就会发现,可以传入一个 boolean 值的参数,控制抢锁是否是公平的。

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

默认是非公平,可以传入 true 来使用公平锁。

三、总结

  1. CountDownLatch 是一个线程等待其他线程, CyclicBarrier 是多个线程互相等待。
  2. CountDownLatch 的计数是减 1 直到 0,CyclicBarrier 是加 1,直到指定值。
  3. CountDownLatch 是一次性的, CyclicBarrier 可以循环利用。
  4. CyclicBarrier 可以在最后一个线程达到屏障之前,选择先执行一个操作。
  5. Semaphore ,需要拿到许可才能执行,并可以选择公平和非公平模式。

最后引用我很佩服的一个人经常说的话:你知道的越多,你不知道的越多!

文章参考:

https://mp.weixin.qq.com/s/TDw7GnzDw5FK3RWwkIzzZA

https://blog.csdn.net/paul342/article/details/49661991

https://blog.csdn.net/tolcf/article/details/50925145

  相关解决方案