写点什么

Java 多线程并发控制工具信号量 Semaphore,实现原理及案例

用户头像
码农架构
关注
发布于: 2021 年 01 月 07 日
Java多线程并发控制工具信号量Semaphore,实现原理及案例

信号量(Semaphore)是 Java 多线程兵法中的一种 JDK 内置同步器,通过它可以实现多线程对公共资源的并发访问控制。一个线程在进入公共资源时需要先获取一个许可,如果获取不到许可则要等待其它线程释放许可,每个线程在离开公共资源时都会释放许可。其实可以将 Semaphore 看成一个计数器,当计数器的值小于许可最大值时,所有调用 acquire 方法的线程都可以得到一个许可从而往下执行。而调用 release 方法则可以让计数器的值减一。


信号量的主要应用场景是控制最多 N 个线程同时地访问资源,其中计数器的最大值即是许可的最大值 N。以停车场为例,假设停车场一共有 8 个车位,其中 6 个车位已被停放,然后来了两辆汽车,此时因为刚好剩下两个车位所以这两辆车都能停放。接着又来了一辆车,现在已经没有空位了所以只能等待其它车离开。此时刚好一辆红色汽车离开停车场,来开后黄车刚好可以停进去,假如又有一辆汽车进来则该车又得等待。如此往复。这个过程中停车场就是公共资源,车位数就是信号量最大许可数,车辆就好比线程。

四要素

信号量的四要素为:最大许可数公平模式acquire 方法以及 release 方法。最大许可数和公平模式在构建 Semaphore 对象时指定,分别表示公共资源最多可以多少个线程同时访问以及获取许可时是否使用公平模式。acquire 方法用于获取许可,假如因为不足许可的话则进入等待状态。release 方法用于释放许可。

非公平模式的实现

Semaphore 类的实现是基于 AQS 同步器来实现的,不管是公平模式还是非公平模式都是基于 AQS 的共享模式,只是在获取许可的操作逻辑有差异。Semaphore 的默认模式为非公平模式,我们先看非公平模式的实现。



Semaphore 类的几个主要方法如下所示,其中提供了两个构造函数,相关的两个参数为许可最大数和是否使用公平模式,其中 FairSync 是公平模式的同步器而 NonfairSync 则是非公平模式的同步器。有两个 acquire 方法,无参时默认是一次获取 1 个许可,而如果传入整型参数则表示一次获取若干个许可。对应地,也有两个 release 方法,无参时表示释放 1 个许可,而整型参数则表示一次释放若干个许可。Semaphore 主要的几个方法如下



Semaphore 内部的 Syn 子类是公平模式 FairSync 类和非公平模式 NonfairSync 类的抽象父类,许可最大数与 AQS 同步器的状态变量对应。因为模式是非公平模式,所以这里提供了非公平的许可获取方法 nonfairTryAcquireShared。非公平模式其实就是在许可数量允许的情况下,让所有线程都进行自旋操作,而不管它们先来后到的顺序,全部线程放到一起去竞争许可。其中 compareAndSetState 方法提供了 CAS 算法从而能够保证并发修改许可值,而剩余许可数等于当前可用许可值减去当前消耗许可数,需要注意的是当剩余许可数小于 0 时则返回负数从而导致线程会进入等待队列中。tryReleaseShared 方法则提供了释放许可的操作,不管是不是公平模式都使用该方法即可,释放许可的逻辑是相同的。通过自旋操作来将释放的许可数增加到当前剩余许可数。



非公平模式 NonfairSync 类的实现主要是 tryAcquireShared 方法,直接调用父类 Sync 的的 nonfairTryAcquireShared 方法即可。


非公平同步器


公平模式实现

公平模式与非公平模式的主要差异就在获取许可时的机制,非公平模式直接通过自旋操作让所有线程竞争许可,从而导致了非公平。而公平模式则通过队列来实现公平机制。它们的差异就在 tryAcquireShared 方法,我们看公平模式的 tryAcquireShared 方法。实际上不同的地方就在下图中加了方框的两行代码,它会检查是否已经存在等待队列,如果已经有等待队列则返回-1,返回-1 则表示让 AQS 同步器将当前线程进入等待队列中,队列则意味着公平。实际上,这也并非是严格的公平,在前面讲到的 AQS 同步器的公平性章节有深入讲过 AQS 的公平性,如果忘记了可以重新查阅加深理解。而且在为达到最大许可数的情况下,所有线程也并没有进入等待队列中,而是全部线程进行自旋获取许可。

公平模式的实现

案例 1 

我们先看一个简单的例子,首先实例化一个拥有 5 个许可的信号量对象,然后一共有 10 个线程一同尝试获取 5 个许可,得到许可的线程将 value 进行累加 1,接着睡眠五秒,最后释放许可。

以上程序输出如下,其中有五个线程输出“counting number : xx”后其他线程则开始等待。大概等待 5 秒后获得许可的五个线程执行释放许可操作,然后其它线程才能获得许可并往下执行。

案例 2 

例子二与例子一很相似,不同的地方在于每次获取许可时会消耗 2 个许可,同样释放时也释放 2 个许可。这里实例化一个拥有 6 个许可的信号量对象,然后 10 个线程一同尝试获取许可。但这次最多只能同时 3 个线程得到许可,也就是三个线程得到许可后对 value 值进行累加 1,然后睡眠 5 秒后释放许可。接着另外三个线程又获得许可往下执行,直到 10 个线程都执行完。



总结 

本文介绍了一个 JDK 内置的同步器——信号量(Semaphore),通过它能够控制最多若干个线程访问公共资源。它可以看成是一个计数器,当计数器的值小于许可最大值时线程能够往下执行,反之线程则只能等待。我们深入分析了 Semaphore 的实现原理,它基于 AQS 同步器进行实现且提供了公平和非公平两种模式,并且我们对这两种模式的实现分别进行了分析。通过本文我们已经能够很深入清晰理解 Semaphore 的原理机制了。


发布于: 2021 年 01 月 07 日阅读数: 35
用户头像

码农架构

关注

公众号:码农架构 2018.03.22 加入

专注于系统架构、高可用、高性能、高并发类技术分享

评论 (1 条评论)

发布
用户头像
这篇文章对你有帮助吗?作为一名程序工程师,在评论区留下你的困惑或你的见解,大家一起来交流吧!
2021 年 01 月 20 日 10:02
回复
没有更多了
Java多线程并发控制工具信号量Semaphore,实现原理及案例