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

本文主要是介绍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方法进行重置

这篇关于java 信号量 countdown_Java并发包CountDownLatch、CyclicBarrier、Semaphore原理解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/362848

相关文章

SpringBoot中四种AOP实战应用场景及代码实现

《SpringBoot中四种AOP实战应用场景及代码实现》面向切面编程(AOP)是Spring框架的核心功能之一,它通过预编译和运行期动态代理实现程序功能的统一维护,在SpringBoot应用中,AO... 目录引言场景一:日志记录与性能监控业务需求实现方案使用示例扩展:MDC实现请求跟踪场景二:权限控制与

Java NoClassDefFoundError运行时错误分析解决

《JavaNoClassDefFoundError运行时错误分析解决》在Java开发中,NoClassDefFoundError是一种常见的运行时错误,它通常表明Java虚拟机在尝试加载一个类时未能... 目录前言一、问题分析二、报错原因三、解决思路检查类路径配置检查依赖库检查类文件调试类加载器问题四、常见

Java注解之超越Javadoc的元数据利器详解

《Java注解之超越Javadoc的元数据利器详解》本文将深入探讨Java注解的定义、类型、内置注解、自定义注解、保留策略、实际应用场景及最佳实践,无论是初学者还是资深开发者,都能通过本文了解如何利用... 目录什么是注解?注解的类型内置注编程解自定义注解注解的保留策略实际用例最佳实践总结在 Java 编程

Java 实用工具类Spring 的 AnnotationUtils详解

《Java实用工具类Spring的AnnotationUtils详解》Spring框架提供了一个强大的注解工具类org.springframework.core.annotation.Annot... 目录前言一、AnnotationUtils 的常用方法二、常见应用场景三、与 JDK 原生注解 API 的

Java controller接口出入参时间序列化转换操作方法(两种)

《Javacontroller接口出入参时间序列化转换操作方法(两种)》:本文主要介绍Javacontroller接口出入参时间序列化转换操作方法,本文给大家列举两种简单方法,感兴趣的朋友一起看... 目录方式一、使用注解方式二、统一配置场景:在controller编写的接口,在前后端交互过程中一般都会涉及

Java中的StringBuilder之如何高效构建字符串

《Java中的StringBuilder之如何高效构建字符串》本文将深入浅出地介绍StringBuilder的使用方法、性能优势以及相关字符串处理技术,结合代码示例帮助读者更好地理解和应用,希望对大家... 目录关键点什么是 StringBuilder?为什么需要 StringBuilder?如何使用 St

使用Java将各种数据写入Excel表格的操作示例

《使用Java将各种数据写入Excel表格的操作示例》在数据处理与管理领域,Excel凭借其强大的功能和广泛的应用,成为了数据存储与展示的重要工具,在Java开发过程中,常常需要将不同类型的数据,本文... 目录前言安装免费Java库1. 写入文本、或数值到 Excel单元格2. 写入数组到 Excel表格

redis中使用lua脚本的原理与基本使用详解

《redis中使用lua脚本的原理与基本使用详解》在Redis中使用Lua脚本可以实现原子性操作、减少网络开销以及提高执行效率,下面小编就来和大家详细介绍一下在redis中使用lua脚本的原理... 目录Redis 执行 Lua 脚本的原理基本使用方法使用EVAL命令执行 Lua 脚本使用EVALSHA命令

Java并发编程之如何优雅关闭钩子Shutdown Hook

《Java并发编程之如何优雅关闭钩子ShutdownHook》这篇文章主要为大家详细介绍了Java如何实现优雅关闭钩子ShutdownHook,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 目录关闭钩子简介关闭钩子应用场景数据库连接实战演示使用关闭钩子的注意事项开源框架中的关闭钩子机制1.

Maven中引入 springboot 相关依赖的方式(最新推荐)

《Maven中引入springboot相关依赖的方式(最新推荐)》:本文主要介绍Maven中引入springboot相关依赖的方式(最新推荐),本文给大家介绍的非常详细,对大家的学习或工作具有... 目录Maven中引入 springboot 相关依赖的方式1. 不使用版本管理(不推荐)2、使用版本管理(推