Java并发工具之Semaphore

article/2025/11/7 13:27:28

一、简介

摘自《Java并发编程的艺术》一书

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源

Semaphore一般用于流量的控制,特别是公共资源有限的应用场景。例如数据库的连接,假设数据库的连接数上线为10个,多个线程并发操作数据库可以使用Semaphore来控制并发操作数据库的线程个数最多为10个。

二、类总览

通过上面的类图可以看到,Semaphore与ReentrantLock的内部类的结构相同,类内部总共存在Sync、NonfairSync、FairSync三个类,NonfairSync与FairSync类继承自Sync类,Sync类继承自AbstractQueuedSynchronizer抽象类

1. 类继承关系

public class Semaphore implements java.io.Serializable {}

实现了Serializable接口

2. 类的内部类

2.1 Sync类

abstract static class Sync extends AbstractQueuedSynchronizer {//序列化版本号private static final long serialVersionUID = 1192457210091910933L;//构造方法Sync(int permits) {setState(permits);}//获取许可final int getPermits() {return getState();}//共享模式下非公平策略获取final int nonfairTryAcquireShared(int acquires) {for (;;) {int available = getState();int remaining = available - acquires;if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}}//共享模式下进行释放protected final boolean tryReleaseShared(int releases) {for (;;) {int current = getState();int next = current + releases;if (next < current) // overflowthrow new Error("Maximum permit count exceeded");if (compareAndSetState(current, next))return true;}}//根据指定的缩减量减小可用许可的数目final void reducePermits(int reductions) {for (;;) {int current = getState();int next = current - reductions;if (next > current) // underflowthrow new Error("Permit count underflow");if (compareAndSetState(current, next))return;}}//获取并返回立即可用的所有许可final int drainPermits() {for (;;) {int current = getState();if (current == 0 || compareAndSetState(current, 0))return current;}}
}

方法细节第四章会详细分析

2.2 NonfairSync类

NonfairSync类继承了Sync类,表示采用非公平策略获取资源,其只有一个tryAcquireShared方法,重写了AQS的该方法

static final class NonfairSync extends Sync {//序列化版本号private static final long serialVersionUID = -2694183684443567898L;//构造方法NonfairSync(int permits) {super(permits);}//共享模式下获取protected int tryAcquireShared(int acquires) {return nonfairTryAcquireShared(acquires);}
}

2.3 FairSync类

FairSync类继承了Sync类,表示采用公平策略获取资源,其只有一个tryAcquireShared方法,重写了AQS的该方法

static final class FairSync extends Sync {//序列化版本号private static final long serialVersionUID = 2014338818796000944L;//构造方法FairSync(int permits) {super(permits);}//共享模式下获取protected int tryAcquireShared(int acquires) {for (;;) {if (hasQueuedPredecessors())return -1;int available = getState();int remaining = available - acquires;if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}}
}

3. 类的属性

CountDownLatch类似,Semaphore主要是通过AQS的共享锁机制实现的,因此它的核心属性只有一个sync

//序列化版本号
private static final long serialVersionUID = -3222578661600680210L;
//同步队列
private final Sync sync;

4. 构造方法

Semaphore构造方法有两种

//指定许可数,默认为非公平策略
public Semaphore(int permits) {sync = new NonfairSync(permits);
}//指定许可数和是否公平策略
public Semaphore(int permits, boolean fair) {sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}

构造方法类似于ReentrantLock的构造方法,只是多了一个许可数的参数。调用构造方法的结果就是初始化了同步队列实例,设置state值为permits值

三、使用案例

这里以经典的停车作为案例。假设停车场有3个停车位,此时有5辆汽车需要进入停车场停车。

public static void main(String[] args) {//定义semaphore实例,设置许可数为3,即停车位为3个Semaphore semaphore = new Semaphore(3);//创建五个线程,即有5辆汽车准备进入停车场停车for (int i = 1; i <= 5; i++) {new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + "尝试进入停车场...");//尝试获取许可semaphore.acquire();//模拟停车long time = (long) (Math.random() * 10 + 1);System.out.println(Thread.currentThread().getName() + "进入了停车场,停车" + time +"秒...");Thread.sleep(time);} catch (InterruptedException e) {e.printStackTrace();} finally {System.out.println(Thread.currentThread().getName() + "开始驶离停车场...");//释放许可semaphore.release();System.out.println(Thread.currentThread().getName() + "离开了停车场!");}}, i + "号汽车").start();}
}

执行结果如下

1号汽车尝试进入停车场...
5号汽车尝试进入停车场...
4号汽车尝试进入停车场...
3号汽车尝试进入停车场...
2号汽车尝试进入停车场...
5号汽车进入了停车场,停车5秒...
1号汽车进入了停车场,停车8秒...
4号汽车进入了停车场,停车9秒...
5号汽车开始驶离停车场...
5号汽车离开了停车场!
3号汽车进入了停车场,停车10秒...
1号汽车开始驶离停车场...
1号汽车离开了停车场!
2号汽车进入了停车场,停车2秒...
4号汽车开始驶离停车场...
4号汽车离开了停车场!
2号汽车开始驶离停车场...
2号汽车离开了停车场!
3号汽车开始驶离停车场...
3号汽车离开了停车场!

总结,每个线程调用acquire方法尝试获取许可,如果成功就会继续执行,否则就会被阻塞,当获取许可成功后,调用release方法释放许可供其他线程使用,之前被阻塞的线程会被唤醒继续执行。

四、核心方法

通过上面的案例我们知道Semaphore的核心方法是acquire获取信号量和release释放信号量,下面我们来一一分析

1. acquire

Semaphore提供了acquire方法来获取一个许可。老规矩,我们跟着上面的案例进入源码分析,acquire方法定义如下

public void acquire() throws InterruptedException {sync.acquireSharedInterruptibly(1);
}

内部调用的是AQS的acquireSharedInterruptibly方法, 即共享式获取响应中断,定义如下

public final void acquireSharedInterruptibly(int arg)throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();if (tryAcquireShared(arg) < 0)doAcquireSharedInterruptibly(arg);
}

该方法在AQS详解中有讲解过,这里不赘述了,我们来分析一下子类实现的tryAcquireShared方法,这里就要分公平和非公平策略两种情况了

1.1 非公平策略下

非公平策略下的tryAcquireShared方法定义如下

protected int tryAcquireShared(int acquires) {return nonfairTryAcquireShared(acquires);
}

内部调用Sync类中的nonfairTryAcquireShared方法

final int nonfairTryAcquireShared(int acquires) {//自旋for (;;) {//获取可用许可值int available = getState();//计算剩余的许可值int remaining = available - acquires;//如果剩余许可值小于0,说明许可不够用了,直接返回,否则CAS更新同步状态,更新成功返回,否则继续自旋if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}
}

该方法本质就是一个自旋方法,通过自旋+CAS来保证修改许可值的线程安全性。该方法返回的情况有如下两种情况

  • 信号量不够,直接返回,返回值为负数,表示获取失败
  • 信号量足够,且CAS操作成功,返回值为剩余许可值,获取成功

1.2 公平策略下

公平策略下的tryAcquireShared方法定义如下

protected int tryAcquireShared(int acquires) {//自旋for (;;) {if (hasQueuedPredecessors())return -1;int available = getState();int remaining = available - acquires;if (remaining < 0 ||compareAndSetState(available, remaining))return remaining;}
}

我们看到它与非公平策略的唯一区别就是多了下面这个if代码

if (hasQueuedPredecessors())return -1;

即在获取共享锁之前,先调用hasQueuedPredecessors方法来判断队列中是否存在其他正在排队的节点,如果是返回true,否则为false。因此当存在其他正在排队的节点,当前节点就无法获取许可,只能排队等待,这也是公平策略的体现。

2. release

Semaphore提供release来释放许可。我们继续分析release方法,定义如下

public void release() {sync.releaseShared(1);
}

调用AQS的releaseShared方法,即释放共享式同步状态

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;//如果释放后比释放前的许可值还小,直接报Errorif (next < current) // overflowthrow new Error("Maximum permit count exceeded");//CAS修改许可值,成功则返回,失败则继续自旋if (compareAndSetState(current, next))return true;}
}

该方法也是一个自旋方法,通过自旋+CAS原子性地修改同步状态,逻辑很简单。

3. 其余方法

获取信号量的方法总共有四个

方法说明
acquire()获取信号量,默认获取1个许可,响应中断sync.acquireSharedInterruptibly(1)
acquire(int permits)获取信号量,指定获取许可的个数,响应中断sync.acquireSharedInterruptibly(permits)
acquireUninterruptibly()获取信号量,默认获取1个许可,不响应中断sync.acquireShared(1)
acquireUninterruptibly(int permits)获取信号量,指定获取许可的个数,不响应中断sync.acquireShared(permits)

释放信号量的方法有两个

方法说明
release()释放信号量,默认释放一个许可,等同于release(1)
release(int permits)释放信号量,指定释放许可的个数

获取信号量四个方法中后面三个方法原理同acquire(),小伙伴可以自己去源码中探究,这里就不再赘述了。我们下面看一下其余的工具方法

3.1 tryAcquire

tryAcquire方法一共有四种重载形式

  • tryAcquire()

    public boolean tryAcquire() {return sync.nonfairTryAcquireShared(1) >= 0;
    }

    tryAcquireacquire方法类似,只是tryAcquire的意思是尝试获取许可,如果获取成功返回true,否则返回false,不会阻塞线程,而且不响应中断

  • tryAcquire(int permits)

    public boolean tryAcquire(int permits) {if (permits < 0) throw new IllegalArgumentException();return sync.nonfairTryAcquireShared(permits) >= 0;
    }

    同上,可以指定获取许可的个数

  • tryAcquire(long timeout, TimeUnit unit)

    public boolean tryAcquire(long timeout, TimeUnit unit)throws InterruptedException {return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    tryAcquire可以指定超时时间,它调用AQS的tryAcquireSharedNanos方法,即共享式超时获取。同样该方法在AQS详解中有讲解过,不再赘述。

  • tryAcquire(int permits, long timeout, TimeUnit unit)

    public boolean tryAcquire(int permits, long timeout, TimeUnit unit)throws InterruptedException {if (permits < 0) throw new IllegalArgumentException();return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
    }

    同上,可以指定获取许可的个数

3.2 availablePermits

获取可用许可数

public int availablePermits() {//获取可用许可数return sync.getPermits();
}//获取可用许可数
final int getPermits() {return getState();
}

3.3 drainPermits

将剩下的信号量一次性消耗光,并且返回所消耗的信号量

public int drainPermits() {return sync.drainPermits();
}

调用Sync类的drainPermits方法,定义如下

final int drainPermits() {//自旋操作for (;;) {//获取信号量值int current = getState();//如果信号量为0,直接返回//否则CAS修改为0,成功则返回,否则继续自旋if (current == 0 || compareAndSetState(current, 0))return current;}
}

3.4 reducePermits

减少信号量的总数

protected void reducePermits(int reduction) {if (reduction < 0) throw new IllegalArgumentException();sync.reducePermits(reduction);
}

调用Sync类的reducePermits方法,定义如下

final void reducePermits(int reductions) {//自旋for (;;) {//获取当前信号量值int current = getState();//计算剩余许可值int next = current - reductions;if (next > current) // underflowthrow new Error("Permit count underflow");//CAS修改同步状态,成功则返回,失败则继续自旋if (compareAndSetState(current, next))return;}
}

reducePermits方法本质就是减少指定的信号量的值,它和acquire方法相比都是减少信号量的值,但是reducePermits不会导致任何线程阻塞,即只要传递的参数reductions(减少的信号量的数量)大于0,操作就会成功。所以调用该方法可能会导致信号量最终为负数

五、总结

Semaphore是一个有效的流量控制工具,它基于AQS共享锁实现。我们常常用它来控制对有限资源的访问

  • 每次使用资源前,先申请一个信号量,如果资源数不够,就会阻塞等待;
  • 每次释放资源后,就释放一个信号量

http://chatgpt.dhexx.cn/article/QWCw1Ebg.shtml

相关文章

Java 并发编程实战-创建和执行任务的最佳实践

若无法通过并行流实现并发&#xff0c;则必须创建并运行自己的任务。运行任务的理想Java 8方法就是CompletableFuture。 Java并发的历史始于非常原始和有问题的机制&#xff0c;并且充满各种尝试的优化。本文将展示一个规范形式&#xff0c;表示创建和运行任务的最简单&#x…

java并发-java并发大师

文章目录 java并发大师James GoslingDoug Lea 参考 java并发大师 聊聊java&#xff08;十&#xff09;Java并发大师Brain Goetz和Doug Lea 的中英文博客文章地址 参考URL: https://blog.csdn.net/weixin_33963594/article/details/92481739 James Gosling 技术大牛收割机&am…

java并发编程之 并发问题及解决方法

一、并发问题的根源 首先&#xff0c;我们要知道并发要解决的是什么问题&#xff1f;并发要解决的是单进程情况下硬件资源无法充分利用的问题。而造成这一问题的主要原因是CPU-内存-磁盘三者之间速度差异实在太大。如果将CPU的速度比作火箭的速度&#xff0c;那么内存的速度就…

『图解Java并发编程系列』10张图告诉你Java并发多线程那些破事

目录 线程安全问题 活跃性问题 性能问题 有态度的总结 头发很多的程序员&#xff1a;『师父&#xff0c;这个批量处理接口太慢了&#xff0c;有什么办法可以优化&#xff1f;』架构师&#xff1a;『试试使用多线程优化』第二天头发很多的程序员&#xff1a;『师父&#xff…

Java基础-并发篇

3.1. JAVA 并发知识库 3.2. JAVA 线程实现/创建方式 3.2.1. 继承 Thread 类 ​ Thread 类本质上是实现了 Runnable 接口的一个实例&#xff0c;代表一个线程的实例。启动线程的唯一方 法就是通过 Thread 类的 start()实例方法。start()方法是一个 native 方法&#xff0c;它将…

java并发总结

一、并发基础 ㅤ 1、进程与线程 ㅤ 进程 程序由指令和数据组成&#xff0c;但这些指令要运行&#xff0c;数据要读写&#xff0c;就必须将指令加载至 CPU&#xff0c;数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 I…

pytorch 图片分类,python 图片分类,resnet18 图片分类

pytorch 图片分类&#xff0c;python 图片分类&#xff0c;resnet18 图片分类&#xff0c;深度学习 图片分类 pytorch版本&#xff1a;1.5.0cu101 全部源码&#xff0c;可以直接运行。 下载地址&#xff1a;https://download.csdn.net/download/TangLingBo/12598435 网络是…

深度学习图片分类实战学习

开始记录学习深度学习的点点滴滴 深度学习图片分类实战学习 前言一、深度学习二、使用步骤1. 自建网络模型2. 进行深度学习的学习迁移 注意事项 前言 随着人工智能的不断发展&#xff0c;这门技术也越来越重要&#xff0c;很多人都开启了学习人工智能&#xff0c;本人开始记录…

关于图片的多标签分类(1)

最近还在处理人脸附件&#xff08;眼镜&#xff0c;刘海&#xff0c;口罩&#xff0c;帽子&#xff09;的multi-label分类。给自己普及一下常识性问题&#xff1a; 1&#xff09;什么是multi-label分类&#xff1f; multi-label分类&#xff0c;常见一张图片中可以存在多个目…

svm实现图片分类(python)

目录 前言 knn vs. svm svm & linear classifier bias trick loss function regularization optimization 代码主体 导入数据及预处理 svm计算loss_function和梯度 验证梯度公式是否正确 比较运行时间 svm训练及预测&#xff0c;结果可视化 通过corss-validat…

图片分类-python

目的&#xff1a;做一个简易的图片分类。 使用到的算法&#xff1a;hog、surfsvm 图片集&#xff1a;cifar-10、cifar-100、stl-10、自制图片集 分类完整代码链接 使用说明&#xff1a; 1.cifar-10、cifar-100和stl-10直接解压 2.自制图片集文件夹结构&#xff1a; ├…

CNN图片分类

最近在阅读一些AI项目&#xff0c;写入markdown&#xff0c;持续更新&#xff0c;算是之后也能回想起做法 项目 https://github.com/calssion/Fun_AI image classify(图片分类) CNN classify dogs and cats(猫狗二分类) Tutorial(教程):https://developers.google.com/mach…

深度学习之图像分类

第一篇CSDN文章&#xff0c;写的不好&#xff0c;还请各位大佬指正。万事开头难&#xff0c;千里之行始于足下&#xff01; 1.什么是图像分类 图像分类&#xff0c;核心是从给定的分类集合中给图像分配一个标签的任务。实际上&#xff0c;这意味着我们的任务是分析一个输入图…

关于图像分类

https://www.zhihu.com/question/57075015/answer/194397802https://www.zhihu.com/question/57075015/answer/194397802 先定义一下图像分类&#xff0c;一般而言&#xff0c;图像分类分为通用类别分类以及细粒度图像分类 那什么是通用类别以及细粒度类别呢&#xff1f;这里…

(一)图像分类任务介绍 Image Classification

目录 一、什么是图像分类任务&#xff1f;它有哪些应用场景&#xff1f; 二、图像分类任务的难点&#xff1f; 三、基于规则的方法是否可行&#xff1f; 四、什么是数据驱动的图像分类范式&#xff1f; 数据集构建 分类器设计与学习 分类器决策 五、常用的分类任务评价指…

图像分类的数据集

图像分类的数据集 1. MNIST2. Fashion-MNIST3.CIFAR-10和CIFAR-1004. Caltech 1015. ImageNet5.1 ImageNet是什么&#xff1f;5.2 ILSVRC 6. 各个数据集上的最新进展其他参考资料 1. MNIST MNIST数据集的一个样例 一般机器学习框架都使用MNIST作为入门&#xff0c;就像"He…

机器学习——图像分类

1 图像分类的概念 1.1 什么是图像分类&#xff1f; 图像分类&#xff0c;根据图像信息中所反映出来的不同特征&#xff0c;把不同类别的目标区分开来的图像处理方法 1.2 图像分类的难度 ●任何拍摄情 况的改变都将提升分类的难度 1.3 CNN如何进行图像分类 ●数据驱动型方法通…

图像分类算法

图像分类 参考链接1.前言2.K近邻与KMeans算法比较KNN原理和实现过程(1) 计算已知类别数据集中的点与当前点之间的距离&#xff1a;(2) 按照距离递增次序排序(3) 选取与当前点距离最小的k个点(4) 确定前k个点所在类别的出现频率(5) 返回前k个点出现频率最高的类别作为当前点的预…

图像分类方法总结

1. 图像分类问题描述 图像分类问题是计算机视觉领域的基础问题&#xff0c;它的目的是根据图像的语义信息将不同类别图像区分开来&#xff0c;实现最小的分类误差。具体任务要求是从给定的分类集合中给图像分配一个标签的任务。总体来说&#xff0c;对于单标签的图像分类问题&…

9.图片分类数据集

1. 图像分类数据集 MNIST数据集 [LeCun et al., 1998] 是图像分类中广泛使用的数据集之一&#xff0c;但作为基准数据集过于简单。 我们将使用类似但更复杂的Fashion-MNIST数据集。 %matplotlib inline import torch import torchvision from torch.utils import data from t…