Java多线程之Semaphore实现信号灯
作者:冬日毛毛雨 发布时间:2023-09-19 23:04:51
目录
1 Semaphore的主要方法
2 实例讲解
实现单例模式
3 源码解析
构造方法
获取许可
释放许可
减小许可数量
获取剩余许可数量
前言:
Semaphore是计数信号量。Semaphore管理一系列许可证。每个acquire方法阻塞,直到有一个许可证可以获得然后拿走一个许可证;每个release方法增加一个许可证,这可能会释放一个阻塞的acquire方法。然而,其实并没有实际的许可证这个对象,Semaphore只是维持了一个可获得许可证的数量。
Semaphore可以维护当前访问自身的线程个数,并提供了同步机制。使用Semaphore可以控制同时访问资源的线程个数,例如,实现一个文件允许的并发访问数。
1 Semaphore的主要方法
Semaphore(int permits):
构造方法,创建具有给定许可数的计数信号量并设置为非公平信号量。
Semaphore(int permits,boolean fair):
构造方法,当fair
等于true
时,创建具有给定许可数的计数信号量并设置为公平信号量。
void acquire():
当前线程尝试去阻塞的获取1个许可证。
此过程是阻塞的,它会一直等待许可证,直到发生以下任意一件事:
当前线程获取了1个可用的许可证,则会停止等待,继续执行。
当前线程被中断,则会抛出
InterruptedException
异常,并停止等待,继续执行。
void acquire(int n):
从此信号量获取给定数目许可,在提供这些许可前一直将线程阻塞。
当前线程尝试去阻塞的获取多个许可证。
此过程是阻塞的,它会一直等待许可证,直到发生以下任意一件事:
当前线程获取了n个可用的许可证,则会停止等待,继续执行。
当前线程被中断,则会抛出
InterruptedException
异常,并停止等待,继续执行。
void release():
释放一个许可,将其返回给信号量。
void release(int n):
释放n个许可。
int availablePermits():
当前可用的许可数。void acquierUninterruptibly():
当前线程尝试去阻塞的获取1个许可证(不可中断的)。
此过程是阻塞的,它会一直等待许可证,直到发生以下任意一件事:
当前线程获取了1个可用的许可证,则会停止等待,继续执行。
void acquireUninterruptibly(permits):
当前线程尝试去阻塞的获取多个许可证。
此过程是阻塞的,它会一直等待许可证,直到发生以下任意一件事:
当前线程获取了n个可用的许可证,则会停止等待,继续执行。
boolean tryAcquire()
当前线程尝试去获取1个许可证。
此过程是非阻塞的,它只是在方法调用时进行一次尝试。
如果当前线程获取了1个可用的许可证,则会停止等待,继续执行,并返回
true
。如果当前线程没有获得这个许可证,也会停止等待,继续执行,并返回
false
。
boolean tryAcquire(permits):
当前线程尝试去获取多个许可证。
此过程是非阻塞的,它只是在方法调用时进行一次尝试。
如果当前线程获取了
permits
个可用的许可证,则会停止等待,继续执行,并返回true
。如果当前线程没有获得
permits
个许可证,也会停止等待,继续执行,并返回false
。
boolean tryAcquire(timeout,TimeUnit):
当前线程在限定时间内,阻塞的尝试去获取1个许可证。
此过程是阻塞的,它会一直等待许可证,直到发生以下任意一件事:
当前线程获取了可用的许可证,则会停止等待,继续执行,并返回
true
。当前线程等待时间
timeout
超时,则会停止等待,继续执行,并返回false
。当前线程在
timeout
时间内被中断,则会抛出InterruptedException
一次,并停止等待,继续执行。
boolean tryAcquire(permits,timeout,TimeUnit):
当前线程在限定时间内,阻塞的尝试去获取permits
个许可证。
此过程是阻塞的,它会一直等待许可证,直到发生以下任意一件事:
当前线程获取了可用的
permits
个许可证,则会停止等待,继续执行,并返回true
。当前线程等待时间
timeout
超时,则会停止等待,继续执行,并返回false
。当前线程在
timeout
时间内被中断,则会抛出InterruptedException
一次,并停止等待,继续执行。
2 实例讲解
public class SemaphoreTest {
private static final Semaphore semaphore = new Semaphore(3);
public static void main(String[] args) {
Executor executor = Executors.newCachedThreadPool();
String[] name = {"Jack", "Pony", "Larry", "Martin", "James", "ZhangSan","Tree"};
int[] age = {21,22,23,24,25,26,27};
for(int i=0;i<7;i++)
{
Thread t1=new InformationThread(name[i],age[i]);
executor.execute(t1);
}
}
private static class InformationThread extends Thread {
private final String name;
private final int age;
public InformationThread(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public void run() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName()
+ ":大家好,我是" + name + "我今年" + age +
"当前时间段为:" + System.currentTimeMillis());
Thread.sleep(1000);
System.out.println(name + "要准备释放许可证了,当前时间为:" + System.currentTimeMillis());
System.out.println("当前可使用的许可数为:" + semaphore.availablePermits());
System.out.println("是否有正在等待许可证的线程:" + semaphore.hasQueuedThreads());
System.out.println("正在等待许可证的队列长度(线程数量):" + semaphore.getQueueLength());
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
pool-1-thread-1:大家好,我是Jack我今年21当前时间段为:1543498535306
pool-1-thread-3:大家好,我是Larry我今年23当前时间段为:1543498535306
pool-1-thread-2:大家好,我是Pony我今年22当前时间段为:1543498535306
Pony要准备释放许可证了,当前时间为:1543498536310
Jack要准备释放许可证了,当前时间为:1543498536310
当前可使用的许可数为:0
Larry要准备释放许可证了,当前时间为:1543498536310
是否有正在等待许可证的线程:true
当前可使用的许可数为:0
是否有正在等待许可证的线程:true
正在等待许可证的队列长度(线程数量):4
正在等待许可证的队列长度(线程数量):4
当前可使用的许可数为:0
pool-1-thread-4:大家好,我是Martin我今年24当前时间段为:1543498536311
是否有正在等待许可证的线程:true
pool-1-thread-5:大家好,我是James我今年25当前时间段为:1543498536311
正在等待许可证的队列长度(线程数量):2
pool-1-thread-6:大家好,我是ZhangSan我今年26当前时间段为:1543498536312
James要准备释放许可证了,当前时间为:1543498537315
Martin要准备释放许可证了,当前时间为:1543498537315
当前可使用的许可数为:0
是否有正在等待许可证的线程:true
正在等待许可证的队列长度(线程数量):1
当前可使用的许可数为:0
是否有正在等待许可证的线程:false
pool-1-thread-7:大家好,我是Tree我今年27当前时间段为:1543498537316
正在等待许可证的队列长度(线程数量):0
ZhangSan要准备释放许可证了,当前时间为:1543498537317
当前可使用的许可数为:1
是否有正在等待许可证的线程:false
正在等待许可证的队列长度(线程数量):0
Tree要准备释放许可证了,当前时间为:1543498538319
当前可使用的许可数为:2
是否有正在等待许可证的线程:false
正在等待许可证的队列长度(线程数量):0
以上是非公平信号量,将建立Semaphore
对象的语句改为如下语句:
private static final Semaphore semaphore=new Semaphore(3,true);
pool-1-thread-1:大家好,我是Jack我今年21当前时间段为:1543498810563
pool-1-thread-3:大家好,我是Larry我今年23当前时间段为:1543498810564
pool-1-thread-2:大家好,我是Pony我今年22当前时间段为:1543498810563
Jack要准备释放许可证了,当前时间为:1543498811564
当前可使用的许可数为:0
是否有正在等待许可证的线程:true
正在等待许可证的队列长度(线程数量):4
pool-1-thread-4:大家好,我是Martin我今年24当前时间段为:1543498811564
Larry要准备释放许可证了,当前时间为:1543498811568
当前可使用的许可数为:0
Pony要准备释放许可证了,当前时间为:1543498811568
是否有正在等待许可证的线程:true
当前可使用的许可数为:0
正在等待许可证的队列长度(线程数量):3
是否有正在等待许可证的线程:true
pool-1-thread-5:大家好,我是James我今年25当前时间段为:1543498811568
正在等待许可证的队列长度(线程数量):2
pool-1-thread-6:大家好,我是ZhangSan我今年26当前时间段为:1543498811568
Martin要准备释放许可证了,当前时间为:1543498812566
当前可使用的许可数为:0
是否有正在等待许可证的线程:true
正在等待许可证的队列长度(线程数量):1
pool-1-thread-7:大家好,我是Tree我今年27当前时间段为:1543498812566
James要准备释放许可证了,当前时间为:1543498812572
当前可使用的许可数为:0
是否有正在等待许可证的线程:false
正在等待许可证的队列长度(线程数量):0
ZhangSan要准备释放许可证了,当前时间为:1543498812572
当前可使用的许可数为:1
是否有正在等待许可证的线程:false
正在等待许可证的队列长度(线程数量):0
Tree要准备释放许可证了,当前时间为:1543498813568
当前可使用的许可数为:2
是否有正在等待许可证的线程:false
正在等待许可证的队列长度(线程数量):0
实现单例模式
将创建信号量对象语句修改如下:
private static final Semaphore semaphore=new Semaphore(1);
运行程序,结果如下:
pool-1-thread-1:大家好,我是Jack我今年21当前时间段为:1543499053898
Jack要准备释放许可证了,当前时间为:1543499054903
当前可使用的许可数为:0
是否有正在等待许可证的线程:true
正在等待许可证的队列长度(线程数量):6
pool-1-thread-2:大家好,我是Pony我今年22当前时间段为:1543499054904
Pony要准备释放许可证了,当前时间为:1543499055907
当前可使用的许可数为:0
是否有正在等待许可证的线程:true
正在等待许可证的队列长度(线程数量):5
pool-1-thread-3:大家好,我是Larry我今年23当前时间段为:1543499055907
Larry要准备释放许可证了,当前时间为:1543499056909
当前可使用的许可数为:0
是否有正在等待许可证的线程:true
正在等待许可证的队列长度(线程数量):4
pool-1-thread-4:大家好,我是Martin我今年24当前时间段为:1543499056909
Martin要准备释放许可证了,当前时间为:1543499057913
当前可使用的许可数为:0
是否有正在等待许可证的线程:true
正在等待许可证的队列长度(线程数量):3
pool-1-thread-5:大家好,我是James我今年25当前时间段为:1543499057913
James要准备释放许可证了,当前时间为:1543499058914
当前可使用的许可数为:0
是否有正在等待许可证的线程:true
正在等待许可证的队列长度(线程数量):2
pool-1-thread-6:大家好,我是ZhangSan我今年26当前时间段为:1543499058915
ZhangSan要准备释放许可证了,当前时间为:1543499059919
当前可使用的许可数为:0
是否有正在等待许可证的线程:true
正在等待许可证的队列长度(线程数量):1
pool-1-thread-7:大家好,我是Tree我今年27当前时间段为:1543499059919
Tree要准备释放许可证了,当前时间为:1543499060923
当前可使用的许可数为:0
是否有正在等待许可证的线程:false
正在等待许可证的队列长度(线程数量):0
如上可知,如果将给定许可数设置为1,就如同一个单例模式,即单个停车位,只有一辆车进,然后这辆车出来后,下一辆车才能进。
3 源码解析
Semaphore
有两种模式,公平模式和非公平模式。公平模式就是调用acquire
的顺序就是获取许可证的顺序,遵循FIFO
;而非公平模式是抢占式的,也就是有可能一个新的获取线程恰好在一个许可证释放时得到了这个许可证,而前面还有等待的线程。
构造方法
Semaphore有两个构造方法,如下:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
获取许可
先从获取一个许可看起,并且先看非公平模式下的实现。首先看acquire
方法,acquire
方法有几个重载,但主要是下面这个方法
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
从上面可以看到,调用了Sync
的acquireSharedInterruptibly
方法,该方法在父类AQS
中,如下:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) //如果线程被中断了,抛出异常
throw new InterruptedException();
if (tryAcquireShared(arg) < 0) //获取许可失败,将线程加入到等待队列中
doAcquireSharedInterruptibly(arg);
}
AQS
子类如果要使用共享模式的话,需要实现tryAcquireShared
方法,下面看NonfairSync
的该方法实现:
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
该方法调用了父类中的nonfairTyAcquireShared
方法,如下:
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
//获取剩余许可数量
int available = getState();
//计算给完这次许可数量后的个数
int remaining = available - acquires;
//如果许可不够或者可以将许可数量重置的话,返回
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
从上面可以看到,只有在许可不够时返回值才会小于0,其余返回的都是剩余许可数量,这也就解释了,一旦许可不够,后面的线程将会阻塞。看完了非公平的获取,再看下公平的获取,
代码如下:
protected int tryAcquireShared(int acquires) {
for (;;) {
//如果前面有线程再等待,直接返回-1
if (hasQueuedPredecessors())
return -1;
//后面与非公平一样
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
从上面可以看到,FairSync
与NonFairSync
的区别就在于会首先判断当前队列中有没有线程在等待,如果有,就老老实实进入到等待队列;而不像NonfairSync
一样首先试一把,说不定就恰好获得了一个许可,这样就可以插队了。
看完了获取许可后,再看一下释放许可。
释放许可
释放许可也有几个重载方法,但都会调用下面这个带参数的方法,
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
releaseShared
方法在AQS中,如下:
public final boolean releaseShared(int arg) {
//如果改变许可数量成功
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
AQS子类实现共享模式的类需要实现tryReleaseShared
类来判断是否释放成功,
实现如下:
protected final boolean tryReleaseShared(int releases) {
for (;;) {
//获取当前许可数量
int current = getState();
//计算回收后的数量
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
//CAS改变许可数量成功,返回true
if (compareAndSetState(current, next))
return true;
}
}
从上面可以看到,一旦CAS改变许可数量成功,那么就会调用doReleaseShared()
方法释放阻塞的线程。
减小许可数量
Semaphore
还有减小许可数量的方法,该方法可以用于用于当资源用完不能再用时,这时就可以减小许可证。代码如下:
protected void reducePermits(int reduction) {
if (reduction < 0) throw new IllegalArgumentException();
sync.reducePermits(reduction);
}
可以看到,委托给了Sync,Sync的reducePermits方法如下:
final void reducePermits(int reductions) {
for (;;) {
//得到当前剩余许可数量
int current = getState();
//得到减完之后的许可数量
int next = current - reductions;
if (next > current) // underflow
throw new Error("Permit count underflow");
//如果CAS改变成功
if (compareAndSetState(current, next))
return;
}
}
从上面可以看到,就是CAS改变AQS中的state
变量,因为该变量代表许可证的数量。
获取剩余许可数量
Semaphore
还可以一次将剩余的许可数量全部取走,该方法是drain方法,
如下:
public int drainPermits() {
return sync.drainPermits();
}
Sync的实现如下:
final int drainPermits() {
for (;;) {
int current = getState();
if (current == 0 || compareAndSetState(current, 0))
return current;
}
}
可以看到,就是CAS将许可数量置为0。
4、总结
Semaphore
是信号量,用于管理一组资源。其内部是基于AQS
的共享模式,AQS
的状态表示许可证的数量,在许可证数量不够时,线程将会被挂起;而一旦有一个线程释放一个资源,那么就有可能重新唤醒等待队列中的线程继续执行。
来源:https://juejin.cn/post/7019623653927026719


猜你喜欢
- 本文实例讲述了C#读取系统字体颜色与大小的方法。分享给大家供大家参考。具体分析如下:首先,说到字体、颜色,我们应该想到System.Draw
- 使用场景在 Java 应用中,对于访问频率高,更新少的数据,通常的方案是将这类数据加入缓存中。相对从数据库中读取来说,读缓存效率会有很大提升
- 目录前言hibernate-validator基本使用引入依赖编写需要验证对象验证对象属性是否符合要求验证规则空/非空验证bool时间数学字
- 目录多开理论基础多开实现原理解析代码实现:多开包名代码实现:多用户总结多开理论基础app多开常用于做一些不合法的事情,如高羊毛,黑灰产,甚至
- 这篇文章主要介绍了SpringMVC Mybatis配置多个数据源并切换代码详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一
- 本文给大家分享Android里应用版本更新功能这一块的实现。一个好的应用软件都是需要好的维护,从初出版本到最后精品,这个过程需要版本不停的更
- 一.需求使用JAVA实现单链表,使用单链表检测字符串是否是回文串二.需求分析回文串最重要的就是对称,那么最重要的问题就是找到那个中心,用快指
- screenshot截图展示import step1. Add it in your root build.gradle at the en
- 1、回顾一下JDK * 的核心参数如果我们要为target类创建一个【JDK * 对象】,那么我们必须要传入如下三个核心参数加载targ
- 前言最近在工作中遇到了这么一个需求:如何实现 Android 应用前后台切换的监听?下面来一起看看详细的介绍:iOS 内边是可以实现的,Ap
- 1.浅复制与深复制概念 ⑴浅复制(浅克隆) 被复制对象的所有变量都含有与原来的对象相同的值,
- 本文实例为大家分享了java实现捕鱼达人游戏的具体代码,供大家参考,具体内容如下效果图如下:源代码分享:测试类:package game;i
- 前言哎呀,妈呀,又出异常了!俗话说:“代码虐我千百遍,我待代码如初恋”。小Alan最近一直在忙着工作,已经很久没有写写东西来加深自己的理解了
- 一、什么是队列队列是一个有序列表,可以用数组或者链表来实现。遵循先入先出的原则,即:先存入队列的数据,要先取出。后存入的的数据,后取出。看一
- 1、什么是Mybatis?MyBatis是一个优秀的持久层框架,是一个半ORM(对象关系映射)框架,它对jdbc的操作数据库的过程进行封装,
- //Main:using System;using System.Collections.Generic;using System.Linq
- 最近在项目中,一个go服务给前端提供了一个接口,返回json格式数据,其中Int64字段会超出javascript Number可表示的最大
- 🌱1. 什么是反射机制?首先大家应该先了解两个概念,编译期和运行期,编译期就是编译器帮你把源代码翻译成机器能识别的代码,比如编译器把java
- 一、时区的基本概念GMT(Greenwich Mean Time),即格林威治标准时,是东西经零度的地方。人们将地球人为的分为24等份,每一
- ★前言打开久违的Live Writer,又已经好久没写博客了,真的太懒了。废话不多说了,直接进入这次博客的主题--Timer。为什么要写这个