java 信号量 countdown_Java并发包CountDownLatch、CyclicBarrier、Semaphore原理解析

前言

JUC中提供了很多同步工具类,比如CountDownLatch、CyclicBarrier、Semaphore等,都可以作用同步手段来实现多线程之间的同步效果

一、CountDownLatch

1.1、CountDownLatch的使用

CountDownLatch可以理解为是同步计数器,作用是允许一个或多个线程等待其他线程执行完成之后才继续执行,比如打dota、LoL或者王者荣耀时,创建了一个五人房,只有当五个玩家都准备了之后,游戏才能正式开始,否则游戏主线程会一直等待着直到玩家全部准备。在玩家没准备之前,游戏主线程会一直处于等待状态。如果把CountDownLatch比做此场景都话,相当于开始定义了匹配游戏需要5个线程,只有当5个线程都准备完成了之后,主线程才会开始进行匹配操作。

CountDownLatch案例如下:

18463ea371223aeeae3c6ef22dc9eb18.png

执行结果如下:

等待玩家准备中……游戏房间等待玩家加入……线程Thread-2组队准备,还需等待4人准备线程Thread-3组队准备,还需等待3人准备线程Thread-4组队准备,还需等待2人准备线程Thread-5组队准备,还需等待1人准备线程Thread-6组队准备,还需等待0人准备游戏匹配中……游戏房间已锁定……线程Thread-7组队准备,房间已满不可加入线程Thread-8组队准备,房间已满不可加入线程Thread-9组队准备,房间已满不可加入线程Thread-10组队准备,房间已满不可加入线程Thread-11组队准备,房间已满不可加入线程Thread-12组队准备,房间已满不可加入线程Thread-13组队准备,房间已满不可加入

本案例中有两个线程都调用了latch.await()方法,则这两个线程都会被阻塞,直到条件达成。当5个线程调用countDown方法之后,达到了计数器的要求,则后续再执行countDown方法的效果就无效了,因为CountDownLatch仅一次有效。

1.2、CountDownLatch的实现原理

CountDownLatch的实现原理主要是通过内部类Sync来实现的,内部类Sync是AQS的子类,主要是通过重写AQS的共享式获取和释放同步状态方法来实现的。源码如下:

CountDownLatch初始化时需要定义调用count的次数,然后每调用一次countDown方法都会计数减一,源码如下:

e11c125b1829ebae70268c79fa053660.png

可以看出CountDownLatch的实现逻辑全部都是调用内部类Sync的对应方法实现的,Sync源码如下:

32b35acfb551265a84e2c50e0c97601f.png

通过内部类Sync的源码可以分析出,CountDownLatch的实现完整逻辑如下:

1、初始化CountDownLatch实际就是设置了AQS的state为计数的值

2、调用CountDownLatch的countDown方法时实际就是调用AQS的释放同步状态的方法,每调用一次就自减一次state值

3、调用await方法实际就调用AQS的共享式获取同步状态的方法acquireSharedInterruptibly(1),这个方法的实现逻辑就调用子类Sync的tryAcquireShared方法,只有当子类Sync的tryAcquireShared方法返回大于0的值时才算获取同步状态成功,

否则就会一直在死循环中不断重试,直到tryAcquireShared方法返回大于等于0的值,而Sync的tryAcquireShared方法只有当AQS中的state值为0时才会返回1,否则都返回-1,也就相当于只有当AQS的state值为0时,await方法才会执行成功,否则

就会一直处于死循环中不断重试。

总结:

CountDownLatch实际完全依靠AQS的共享式获取和释放同步状态来实现,初始化时定义AQS的state值,每调用countDown实际就是释放一次AQS的共享式同步状态,await方法实际就是尝试获取AQS的同步状态,只有当同步状态值为0时才能获取成功。

二、CyclicBarrier

2.1、CyclicBarrier的使用

CyclicBarrier可以理解为一个循环同步屏障,定义一个同步屏障之后,当一组线程都全部达到同步屏障之前都会被阻塞,直到最后一个线程达到了同步屏障之后才会被打开,其他线程才可继续执行。

还是以dota、LoL和王者荣耀为例,当第一个玩家准备了之后,还需要等待其他4个玩家都准备,游戏才可继续,否则准备的玩家会被一直处于等待状态,只有当最后一个玩家准备了之后,游戏才会继续执行。

CyclicBarrier使用案例如下:

41b8470ca00ed6f8f06a2b2cf90598d8.png

执行结果如下1:

线程Thread-0组队准备,当前1人已准备线程Thread-1组队准备,当前2人已准备线程Thread-2组队准备,当前3人已准备线程Thread-3组队准备,当前4人已准备线程Thread-4组队准备,当前5人已准备线程:Thread-4开始组队游戏线程:Thread-0开始组队游戏线程:Thread-1开始组队游戏线程:Thread-2开始组队游戏线程:Thread-3开始组队游戏线程Thread-5组队准备,当前1人已准备线程Thread-6组队准备,当前2人已准备线程Thread-7组队准备,当前3人已准备线程Thread-8组队准备,当前4人已准备线程Thread-9组队准备,当前5人已准备线程:Thread-9开始组队游戏线程:Thread-5开始组队游戏线程:Thread-7开始组队游戏线程:Thread-6开始组队游戏线程:Thread-8开始组队游戏线程Thread-10组队准备,当前1人已准备线程Thread-11组队准备,当前2人已准备

本案例中定义了达到同步屏障的线程为5个,每当一个线程调用了barrier.await()方法之后表示该线程已达到屏障,此时当前线程会被阻塞,只有当最后一个线程调用了await方法之后,被阻塞的其他线程才会被唤醒继续执行。

另外CyclicBarrier是循环同步屏障,同步屏障打开之后立马会继续计数,等待下一组线程达到同步屏障。而CountDownLatch仅单次有效。

2.2、CyclicBarrier的实现原理

先看下CyclicBarrier的构造方法

c45c2b063dfa8c5a76987f3dedae4831.png

CyclicBarrier的构造方法没有特殊之处,主要是给两个属性parties(总线程数)、count(当前剩余线程数)进行赋值,这里需要两个值的原因是CyclicBarrier提供了重置的功能,当调用reset方法重置时就需要将count值再赋值成parties的值

再看下await方法的实现逻辑

551ac173df81d4d8fd44af943e2f475d.png

264fc7fd40079e723ba48e914913e53b.png

从源码可以看出CyclicBarrier的实现原理主要是通过ReentrantLock和Condition来实现的,主要实现流程如下:

1、创建CyclicBarrier时定义了CyclicBarrier对象需要达到的线程数count

2、每当一个线程执行了await方法时,需要先通过ReentrantLock进行加锁操作,然后对count进行自减操作,操作成功则判断当前count是否为0;

3、如果当前count不为0则调用Condition的await方法使当前线程进入等待状态;

4、如果当前count为0则表示同步屏障已经完全,此时调用Condition的signalAll方法唤醒之前所有等待的线程,并开启循环的下一次同步屏障功能;

5、唤醒其他线程之后,其他线程继续执行剩余的逻辑。

2.3、通过Synchronized和wait/notify实现CyclicBarrier

通过分析了解了CyclicBarrier是通过ReentrantLock和Condition来实现的,而ReentrantLock+Condition在使用上基本上等同于Synchronized+wait/notify,既然如此就可以通过Synchronized+wait/notify来自定义一个CyclicBarrier,话不多说,代码如下:

c6887e754132c8c5d6fe7f681bc7ea44.png

执行结果如下2:

线程Thread-0组队准备,当前1人已准备线程Thread-1组队准备,当前2人已准备线程Thread-2组队准备,当前3人已准备线程Thread-3组队准备,当前4人已准备线程Thread-4组队准备,当前5人已准备线程:Thread-4开始组队游戏线程:Thread-3开始组队游戏线程:Thread-0开始组队游戏线程:Thread-1开始组队游戏线程:Thread-2开始组队游戏线程Thread-5组队准备,当前1人已准备线程Thread-6组队准备,当前2人已准备线程Thread-7组队准备,当前3人已准备线程Thread-8组队准备,当前4人已准备线程Thread-9组队准备,当前5人已准备线程:Thread-9开始组队游戏线程:Thread-7开始组队游戏线程:Thread-5开始组队游戏线程:Thread-6开始组队游戏线程:Thread-8开始组队游戏线程Thread-10组队准备,当前1人已准备线程Thread-11组队准备,当前2人已准备

可以看出实现的效果和CyclicBarrier实现的效果完全一样

三、Semaphore

3.1、Semaphore的使用

Semaphore字面意思是信号量,实际可以看作是一个限流器,初始化Semaphore时就定义好了最大通行证数量,每次调用时调用方法来消耗,业务执行完毕则释放通行证,如果通行证消耗完,再获取通行证时就需要阻塞线程直到有通行证可以获取。

比如银行柜台的窗口,一共有5个窗口可以使用,当窗口都被占用时,后面来的人就需要排队等候,直到有窗口用户办理完业务离开之后后面的人才可继续争取。模拟代码如下:

a6b88c25eba59c424d9a011f61122e95.png

执行结果如下:

初始化5个银行柜台窗口用户Thread-0占用窗口用户Thread-0开始办理业务用户Thread-1占用窗口用户Thread-1开始办理业务用户Thread-2占用窗口用户Thread-2开始办理业务用户Thread-3占用窗口用户Thread-3开始办理业务用户Thread-4占用窗口用户Thread-4开始办理业务用户Thread-0离开窗口用户Thread-5占用窗口用户Thread-5开始办理业务用户Thread-1离开窗口用户Thread-6占用窗口用户Thread-6开始办理业务用户Thread-2离开窗口用户Thread-7占用窗口用户Thread-7开始办理业务用户Thread-3离开窗口用户Thread-8占用窗口用户Thread-8开始办理业务用户Thread-4离开窗口用户Thread-9占用窗口用户Thread-9开始办理业务用户Thread-5离开窗口用户Thread-6离开窗口用户Thread-7离开窗口用户Thread-8离开窗口用户Thread-9离开窗口

可以看出前5个线程可以直接占用窗口,但是后5个线程需要等待前面的线程离开了窗口之后才可占用窗口。

Semaphore调用acquire方法获取许可证,可以同时获取多个,但是也需要对应的释放多个,否则会造成其他线程获取不到许可证的情况。一旦许可证被消耗完,那么线程就需要被阻塞,直到许可证被释放才可继续执行。

另外Semaphore还具有公平模式和非公平模式两种用法,公平模式则遵循FIFO原则先排队的线程先拿到许可证;非公平模式则自行争取。

3.2、Semaphore实现原理

Semaphore的构造方法

1bd08aefcd5429c486d28f169fab9c5d.png

构造方法只有两个参数,一个是许可证总数量,一个是是否为公平模式;默认是非公平模式

Semaphore的实现全部是通过其内部类Sync来实现了,Sync也是AQS的子类,Semaphore的实现方式基本上和ReentrantLock的实现原理如出一辙。

公平模式实现原理:

ee2468f39612582781033cea342442ab.png

公平模式就是当当前线程是AQS同步队列首节点的后继节点时才有权利尝试获取共享式的同步状态,并将同步状态值减去需要占用的许可证数量,如果剩余许可证数量小于0则表示获取失败进入AQS的死循环不停重试;

如果许可证数量大于0并且CAS设置成功了,则返回剩余许可证数量表示抢占许可证成功;

非公平模式实现原理:

看我公平模式的实现基本是就可以猜到非公平模式是如何实现的,只是会少了一步判断当前节点是否是首节点的后继节点而已。

f447e88628bea22be1f976070daa9f95.png

了解完Semaphore的公平模式和非公平模式的占有许可证的方法,再分析释放许可证的方法,不过可以先自行猜测下会是如何实现的,既然获取许可证是通过state字段不断减少来实现的,那么毫无疑问释放许可证就肯定是不断给state增加来实现的。

释放许可证源码如下:

cb4960001b44614579d926974cc4ceda.png

Semaphore的释放许可证实际就是调用AQS的共享式释放同步状态的方法,然后调用内部类Sync重写的AQS的tryReleaseShared方法,实现逻辑就是不停CAS设置state的值加上需要释放的数量,直到CAS成功。这里少了AQS的逻辑解析,有兴趣可自行回顾AQS的共享式释放同步状态的实现原理。

四、Extra Knowledge

4.1、CountDownLatch 和 CyclicBarrier的区别?

CountDownLatch和CyclicBarrier实现的效果看似都是某个线程等待一组线程达到条件之后才可继续执行,但是实际上两者存在很多区别。

1、CountDownLatch阻塞的是调用await()的线程,不会阻塞达到条件的线程;CyclicBarrier阻塞的是达到同步屏障的所有线程

2、CountDownLatch采用倒数计数,定义数量之后,每当一个线程达到要求之后就减一;CyclicBarrier是正数计数,当数量达到定义的数量之后就打开同步屏障

3、CountDownLatch仅单次有效,不可重复使用;CyclicBarrir可以循环重复使用

4、CountDownLatch定义的数量和实际线程数无关,可以有一个线程执行多次countDown();CyclicBarrier定义的数量和实际线程数一致,必须由多个线程都达到要求执行才行(线程调用await()方法之后就会被阻塞,想调用多次也不行的)

5、CountDownLatch是通过内部类Sync继承AQS来实现的;CyclicBarrier是通过重入锁ReentrantLock来实现的

6、CountDownLatch不可重置;CyclicBarrier可以调用reset方法进行重置

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值