countDownLatch、cyclicBarrier、semaphore使用详解

前言

在前面两篇中介绍了AQS的同步队列条件队列的源码实现,这一篇则是来看一下JUC中主要的三个并发工具类:countDownLatch、cyclicBarrier、semaphore。本文不会像之前两篇文章那样从源码角度展开,而是结合工具的API,来看一下这些工具类的使用方法以及场景。

countDownLatch

countDownLatch我们可以理解为是一个计数器,计数器的初始值是线程的可执行的次数(这个实际上并不等于线程数,因为一个线程可以执行多次)。每当一个线程执行完毕后,计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,在countDownLatch减为0之前,会阻塞这些线程,只有countDownLatch减为0的时候,才会停止阻塞这些线程。

countDownLatch一般有两种使用场景:能够使一个线程在等待另外一些线程完成各自工作之后,再继续执行;当然了,也可以反过来,另外一些线程在等待一个线程完成工作之后,执行这些线程。ok,接下来我们就模拟一下这两个场景。

场景一:现在有一个六人会议,当六个人集齐之后开始开会:

public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //模拟需要凑够六个人才可以开始开会
        CountDownLatch countDownLatch = new CountDownLatch(6);
        //倒计时
        for (int i=0;i<6;i++){
            int finalI = i+1;
            executorService.execute(()->{
                try {
                    Thread.sleep((long) (Math.random()*100));
                    System.out.println("人员"+ finalI +"已到达会议室");
                    countDownLatch.countDown();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        countDownLatch.await();
        System.out.println("人到齐了,开会了开会了");
        executorService.shutdown();
}

countDownLatch最主要的就是两个方法:countDown与await,countDown可以让定义的CountDownLatch值每次减一,await的作用就是阻塞,只有countDownLatch的值减为0才会执行await下面的程序。

运行结果:

人员6已到达会议室
人员3已到达会议室
人员4已到达会议室
人员5已到达会议室
人员1已到达会议室
人员2已到达会议室
人到齐了,开会了开会了

那这个时候如果我执行五个线程会咋样?按照我们上面说的,此时countDownLatch的值肯定不会减到0,所以是不会执行await下面的程序的。

人员1已到达会议室
人员3已到达会议室
人员5已到达会议室
人员2已到达会议室
人员4已到达会议室

第二种场景:我们模拟运动员跑步比赛,需要等到裁判发号施令之后开始跑

public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i=0;i<6;i++){
            int finalI = i+1;
            new Thread(()->{
                try {
                    countDownLatch.await();
                    Thread.sleep((long) Math.random()*1000);
                    System.out.println("运动员"+ finalI +"跑到终点");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        //模拟三秒预备时间
        Thread.sleep(3000);
        countDownLatch.countDown();
        System.out.println("裁判发令枪");
}

cyclicBarrier

cyclicBarrier可以译为循环栅栏、循环屏障,他可以让一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。小时候应该都看过七龙珠,只有七颗龙珠都聚齐了,才可以召唤神龙,接下来我们以七龙珠为例来展示下cyclicBarrier。

public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
        CyclicBarrier barrier=new CyclicBarrier(7,()->{
            System.out.println("集齐七颗龙珠,可以召唤神龙");
        });
        for (int i=1;i<=7;i++){
            int num=i;
            new Thread(()->{
                try {
                    System.out.println("已收集到了第"+num+"颗龙珠");
                    barrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)).start();
        }
}

可以看到,在new CyclicBarrier的时候,他有两个参数,一个是CyclicBarrier的值,还有一个则是一个runnable函数,代表着所有的线程到达这个栅栏的时候,就会去执行里面的runnable函数。

看下效果:

已收集到了第3颗龙珠
已收集到了第6颗龙珠
已收集到了第1颗龙珠
已收集到了第7颗龙珠
已收集到了第4颗龙珠
已收集到了第5颗龙珠
已收集到了第2颗龙珠
集齐七颗龙珠,可以召唤神龙

我们在前面说这个CyclicBarrier是循环栅栏,但是这个例子似乎并没有体现出他的循环特性,不过可以在这个基础上进行一些改装:

public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
  CyclicBarrier cyclicBarrier=new CyclicBarrier(7,()->{
            System.out.println("集齐七颗龙珠,召唤神龙");
            System.out.println("神龙已召唤,三年之后再次收集龙珠");
        });
        for (int i=0;i<7;i++){
            int finalI = i;
            new Thread(()->{
                try {
                    System.out.println(Thread.currentThread().getName()+"收集到第"+ finalI +"颗龙珠");
                    cyclicBarrier.await();
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName()+"收到到第"+finalI+"颗龙珠");
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }

    }

这样的话当所有的线程都达到栅栏的时候,就会去重置CyclicBarrier。

Thread-1收集到第1颗龙珠
Thread-4收集到第4颗龙珠
Thread-2收集到第2颗龙珠
Thread-5收集到第5颗龙珠
Thread-6收集到第6颗龙珠
Thread-0收集到第0颗龙珠
Thread-3收集到第3颗龙珠
集齐七颗龙珠,召唤神龙
神龙已召唤,三年之后再次收集龙珠
Thread-5收到到第5颗龙珠
Thread-6收到到第6颗龙珠
Thread-3收到到第3颗龙珠
Thread-2收到到第2颗龙珠
Thread-4收到到第4颗龙珠
Thread-0收到到第0颗龙珠
Thread-1收到到第1颗龙珠
集齐七颗龙珠,召唤神龙
神龙已召唤,三年之后再次收集龙珠

通过上面的例子,发现countDownLatch与CyclicBarrier在使用上好像差不多,既然差不多,那干脆都用countDownLatch罢了。
在这里插入图片描述

当然了,还是有那么点不同的,首先就是countDownLatch是只能用一次,而CyclicBarrier则是可以循环使用的,这一点在上面的例子中也是体现出来了。

其次,CyclicBarrier的计数器由自己控制,而CountDownLatch的计数器则由使用者来控制,在CyclicBarrier中线程调用await方法不仅会将自己阻塞还会将计数器减1,而在CountDownLatch中线程调用await方法只是将自己阻塞而不会减少计数器的值。而且CyclicBarrier也提供了更多的方法,使用起来也是更加灵活些。

semaphore

百度翻译这个semaphore的意思是信号标、信号量。semaphore可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。

举个例子,可以把它理解成我们停车场入口立着的那个显示屏,每有一辆车进入停车场显示屏就会显示剩余车位减1,每有一辆车从停车场出去,显示屏上显示的剩余车辆就会加1,当显示屏上的剩余车位为0时,车辆就无法进入停车场了,直到有一辆车从停车场出去为止。也可以看出来semaphore可以用来使用在有数量访问限制要求的场景下。接下来我们用代码模拟一下如何使用semaphore。

public static void main(String[] args) {
        Semaphore semaphore=new Semaphore(3);//可用资源
        for (int i=1;i<=10;i++){
            new Thread(()->{
                try {
                    System.out.println("===="+Thread.currentThread().getName()+"来到停车场");
                    if (semaphore.availablePermits()==0){
                        System.out.println("停车场暂时没有可用停车位");
                    }
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"成功进入停车场");
                    TimeUnit.SECONDS.sleep(1);
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName()+"离开停车场");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },String.valueOf(i)+"号车").start();
        }
    }

使用Semaphore时,核心方法是acquire与release两个方法,acquire方法我们可以认为就是获取了一个资源,而release方法则是释放了一个资源,此外也提供了一些其他的方法,可以让我们对Semaphore的使用更加灵活。

====4号车来到停车场
====9号车来到停车场
====1号车来到停车场
====8号车来到停车场
9号车成功进入停车场
====7号车来到停车场
====3号车来到停车场
停车场暂时没有可用停车位
====2号车来到停车场
停车场暂时没有可用停车位
====6号车来到停车场
停车场暂时没有可用停车位
====5号车来到停车场
停车场暂时没有可用停车位
停车场暂时没有可用停车位
停车场暂时没有可用停车位
1号车成功进入停车场
4号车成功进入停车场
====10号车来到停车场
停车场暂时没有可用停车位
1号车离开停车场
3号车成功进入停车场
9号车离开停车场
2号车成功进入停车场
4号车离开停车场
6号车成功进入停车场
3号车离开停车场
5号车成功进入停车场
2号车离开停车场
7号车成功进入停车场
6号车离开停车场
8号车成功进入停车场
7号车离开停车场
10号车成功进入停车场
5号车离开停车场
8号车离开停车场
10号车离开停车场

从结果上面来看,还是很符合semaphore的使用介绍。

结尾

以上就是这三个并发工具类的一些使用方法以及场景,总的来说并不复杂,但是光看不练也是不行滴,我觉得可以在这些例子的基础之上,自己能够写一些例子或者用到项目中才能够加深理解。

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值