Semaphore信号量
Semaphore类结构
public class Semaphore implements java.io.Serializable {
//定义内部基类静态私有
private final Sync sync;
//内部基类 依靠AQS实现
abstract static class Sync extends AbstractQueuedSynchronizer{} ;
//非公平锁内部类
static final class NonfairSync extends Sync {} ;
//公平锁内部类
static final class FairSync extends Sync {} ;
//构造函数Semaphore默认是非公平的。
public Semaphore(int permits) {sync = new NonfairSync(permits);}
// 如果需要公平模式 需要显示传入fair=true
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
信号量Semaphore类结构和ReentrantLock的类结构大体一致,除了ReentrantLock实现类Lock接口,都是依靠内部类继承AQS抽象类实现同步状态管理的
Semaphore类中内部基类Sync
1.setState(),getPermits()
// Semaphore 的内部同步器(共享模式) 继承AQS 依靠AQS实现同步功能
abstract static class Sync extends AbstractQueuedSynchronizer {
//根据构造函数传入的参数permits许可值 初始化AQS的state为许可数
Sync(int ) { setState(permits); }
// 获取当前剩余许可数
final int getPermits() { return getState();}
创建 Semaphore 对象时,会调用构造函数并传入许可证数量 permits,state 的初始值就是许可证数量,例如 new Semaphore(5) 的初始 state=5,所以Semaphore 的共享资源多少是我们自型定义的
在AQS的state字段修饰符是volatile的可见性保证:当一个线程修改了volatile变量的值,新值会立即被刷新到主内存中,并且其他线程读取这个变量时可以立即获得最新的值,同时如果state的加减都是CAS 操作,从而确保多线程环境下对 state 的修改是线程安全的
2.nonfairTryAcquireShared(int acquires)方法
非公平模式下共享资源获取的核心实现
// 非公平模式尝试获取共享许可(供 NonfairSync 使用)
final int nonfairTryAcquireShared(int acquires) {
for (;;) {// 自旋(循环直到成功或失败)
int available = getState(); // 获取当前可用许可证数(AQS的state字段)
int remaining = available - acquires;// 计算剩余许可证数
if (remaining < 0 || // 剩余不足,直接返回负数(失败)
compareAndSetState(available, remaining)) {// CAS尝试更新许可证数
return remaining; // 返回剩余许可(可能为负)
}
}
}
返回值语义
- 正数或零:成功获取许可证,返回值表示剩余的许可证数量(可能唤醒后续等待线程)。
- 负数:获取失败(许可证不足),触发线程进入 AQS 同步队列等待
非公平性以及其余特点
不检查等待队列:新请求的线程直接尝试获取许可证,无需排队,可能“插队”到已阻塞的线程之前
高吞吐量:减少线程切换和排队开销,适用于高并发场景(如短任务快速释放资源)
快速失败机制:如果 remaining < 0(许可证不足),立即返回负数,避免无意义的 CAS 竞争
CAS 原子更新:compareAndSetState(available, remaining) 保证并发环境下许可证数量更新的原子性,若 CAS 失败(其他线程修改了许可证数量),继续循环重试
3.tryReleaseShared(int releases)
释放许可值并进行CAS更新
// 释放共享许可(公共逻辑)
protected final boolean tryReleaseShared(int releases) {
for (;;) {// 自旋循环,确保最终成功
int current = getState(); // 获取当前许可总数
int next = current + releases;// 计算释放后的许可总数
if (next < current) throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next)) {// CAS 更新状态
return true; // CAS 更新成功
}
}
}
步骤解析
①.自旋循环
使用 for () 无限循环,确保在高并发场景下,即使 CAS 失败也能不断重试,直到成功。
②.获取当前状态
int current = getState() 获取当前信号量的许可总数(即 AQS 的 state 值)。
③.计算新状态
int next = current + releases 计算释放后的许可总数。例如,若当前 current = 2,释放 releases = 3,则 next = 5。
④.溢出检查
if (next < current) 检查是否发生整数溢出(当释放的许可数极大时可能导致 int 越界)。若溢出,抛出 Error 终止程序,避免逻辑错误。
⑤.CAS 更新状态
调用 compareAndSetState(current, next) 原子性地将 state 从 current 更新为 next。若成功,返回 true;若失败(其他线程已修改状态),继续循环重试。(始终返回 true(除非溢出抛出异常),因为 Semaphore 的释放操作理论上不会失败(与 acquire 不同,释放总能完成))
4.reducePermits(int reductions)
动态减少信号量的总许可数量
final void reducePermits(int reductions) {
for (;;) {// 自旋循环 确保CAS操作成功
int current = getState();// 获取当前可用许可证数(AQS的state字段)
int next = current - reductions;// 计算减少后的剩余许可证数
if (next > current) // 处理溢出(如reductions 为负数)
throw new Error("Permit count underflow");
if (compareAndSetState(current, next))// CAS 原子更新状态
return;//退出循环
}
}
1.自旋 CAS 操作:通过循环和 CAS 原子更新状态,确保线程安全。
2.校验合法性:若 reductions 为负或导致 next > current(如溢出),抛出异常。
3.更新许可值:将总许可数从 current 减少为 next = current - reductions
好处:
1.动态调整: 允许运行时减少许可总量,无需重新创建 Semaphore 对象
2.不影响已获取许可的线程:已调用 acquire() 的线程可继续持有许可,但后续线程可获取的许可减少
3.允许减少后的许可为负:若减少后的许可为负,后续 acquire() 会阻塞,直到许可被释放且足够
4.线程安全:依赖 AQS 的 CAS 操作,确保并发调用的安全性。
注意事项
1.参数校验:reduction 必须为非负数,否则抛出 IllegalArgumentException。
2.潜在死锁:若过度减少许可导致可用许可长期为负,且无线程释放,可能引发线程饥饿。
5.drainPermits()
用于立即获取并返回当前所有可用的许可(Permits),将剩余的许可数量清零
// 清空所有许可(返回实际清空的数量)
final int drainPermits() {
for (;;) {// 自旋循环 确保在并发竞争下最终成功。
int current = getState();// 获取当前可用许可证数(AQS的state字段)
if (current == 0 || compareAndSetState(current, 0)) {
//将状态从当前值 current 原子性地更新为 0,若成功则返回原有许可数量。
return current;
}
}
}
}
这个方法将当前剩余的许可值拿过来占着,比如剩余3个,同时将许可值更新为0,这样其余的线程都没办法使用了,而这3个我拿过来啥也不干就占着,需要恢复调用 release()方法
特性
非阻塞: 立即返回当前剩余许可,无需等待。
原子性: 通过 CAS 保证线程安全。
忽略公平性: 不检查等待队列中的线程,直接抢占所有许可(无论信号量是公平模式还是非公平模
使用场景:当需要暂停某个资源的使用,直到某个条件满足时,可以使用这个方法
资源重置:需要立即回收所有可用资源(例如重置连接池)。
统计剩余资源:快速获取当前可用许可数量(但会直接消耗这些许可)。
强制资源释放:在需要确保后续操作从零开始时使用(如测试环境)。
注意事项
与公平性的冲突:公平模式,drainPermits() 仍会直接抢占所有许可,可能导致等待队列中的线程饥饿
许可恢复:调用 drainPermits() 后,如果需要恢复许可,需显式调用 release()
// 1. 清空许可
int drained = semaphore.drainPermits();
// 2. 业务逻辑处理(如暂停资源访问)
// 3. 显式恢复许可
semaphore.release(drained); // 返还所有借用的许可
返回值语义:返回的是被清空的许可数量(非负值),而非剩余的许可数
不触发唤醒:若其他线程因许可不足正在等待(如 acquire()),调用 drainPermits() 不会唤醒它们。
reducePermits与 drainPermits 的区别
drainPermits():清空所有可用许可(立即返回当前可用值)。
reducePermits():减少总量,可能使获取许可tryAcquireShared返回的可用许可值为负。
非公平锁内部类NonfairSync
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
// 构造函数 传参值 许可值
NonfairSync(int permits) {
super(permits);
}
// 非公平锁的尝试获取锁逻辑是Sync.nonfairTryAcquireShared方法
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
nonfairTryAcquireShared 这个方法在上面Sync内部类中介绍了,这里不复述了,关键点就是非公平体现:不检查等待队列:新请求的线程直接尝试获取许可证,无需排队
公平锁内部类FairSync
static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;
// 构造函数 传参值 许可值
FairSync(int permits) {
super(permits);
}
// 公平模式获取许可:重写AQS里面的钩子方法tryAcquireShared
protected int tryAcquireShared(int acquires) {
for (;;) {//自旋检查 直到有返回值 负值代表失败 正值代表成功
if (hasQueuedPredecessors())/// 关键:检查是否有等待线程
return -1;// 返回-1 代表当前线程需排队
int available = getState();// 获取当前可用许可证数(AQS的state字段)
int remaining = available - acquires;// 计算剩余许可证数
if (remaining < 0 || // 剩余不足,直接返回负数(失败)
compareAndSetState(available, remaining))// CAS尝试更新许可证数
return remaining;// 返回剩余许可(可能为负)
}
}
}
比起非公平锁 多了核心步骤就是获取许可值资源之前,检查前面是否有等待线程,如果有则直接返回-1 代表尝试获取许可失败,开始入队等待,如果前面没有等待线程,那就和非公平锁逻辑一样开始计算剩余许可数够不够用 以及cas操作更新等
Semaphore类其余方法体
1. acquire() 方法族
// 获取 1 个许可(阻塞,可中断)
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1); // 调用 AQS 方法
}
// 获取 N 个许可(阻塞,可中断)
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
底层调用链:
acquireSharedInterruptibly() → tryAcquireShared()(由 FairSync 或 NonfairSync 实现) → 若失败则线程进入 AQS 等待队列
2. tryAcquire() 方法族
// 非阻塞尝试获取 1 个许可
public boolean tryAcquire() {
return sync.nonfairTryAcquireShared(1) >= 0; // 直接尝试,无需排队
}
// 超时尝试获取许可
public boolean tryAcquire(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
非公平特性:tryAcquire() 直接调用 nonfairTryAcquireShared(),允许插队。
公平特性:最终调用FairSync.tryAcquireShared,先检查等待队列是否存在
3. release() 方法族
// 释放 1 个许可
public void release() {
sync.releaseShared(1); // 调用 AQS 方法
}
// 释放 N 个许可
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
底层逻辑:
releaseShared() → tryReleaseShared()(CAS 增加 state) → 唤醒等待队列中的线程。
4. availablePermits()
// 返回当前可用许可数
public int availablePermits() {
return sync.getPermits();//getState
}
semaphore 共享模式的工作流程
简单代码示例
//默认一个许可值为3的信号量
Semaphore semaphore = new Semaphore(3);
@Test
public void test() throws InterruptedException {
//继承Runnable
Runnable r = new Runnable() {
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " 尝试获取锁开始工作...");
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + " 成功获取到锁并剩余许可数:" + semaphore.availablePermits());
//模拟工作耗时2秒
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
// 3. 释放许可
semaphore.release();
System.out.println(Thread.currentThread().getName() + " 释放许可值,当前许可数:" + semaphore.availablePermits());
}
}
};
// 创建5个线程模拟并发请求
List<Thread> threads = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
Thread thread = new Thread(r,("Thread-" + i));
thread.start();
threads.add(thread);
}
// join()的调用者线程主线程 会等待目标子线程执行完毕,防止主线程执行完毕过快,子线程没执 行完就结束了
for (Thread t : threads) {
t.join();
}
}
控制台打印
Thread-1 尝试获取锁开始工作...
Thread-3 尝试获取锁开始工作...
Thread-2 尝试获取锁开始工作...
Thread-2 成功获取到锁并剩余许可数:0
Thread-1 成功获取到锁并剩余许可数:1
Thread-3 成功获取到锁并剩余许可数:1
Thread-5 尝试获取锁开始工作...
Thread-4 尝试获取锁开始工作...
Thread-1 释放许可值,当前许可数:3
Thread-5 成功获取到锁并剩余许可数:2
Thread-4 成功获取到锁并剩余许可数:1
Thread-3 释放许可值,当前许可数:3
Thread-2 释放许可值,当前许可数:3
Thread-4 释放许可值,当前许可数:2
Thread-5 释放许可值,当前许可数:3
acquire() 执行流程
最先开始工作的线程通过
semaphore.acquire() → acquireSharedInterruptibly(1) → 该方法通过(tryAcquireShared(arg) < 0)判断
通过tryAcquireShared(arg) < 0 判断
tryAcquireShared()(由 FairSync 或 NonfairSync 实现) 返回剩余可用许可值(可为负)
为ture 说明当前没有许可值 进入AQS同步队列等待doAcquireSharedInterruptibly
为false 说明当前线程已经拿到许可值执行任务了(CAS操作变更许可值了) 不需要入队了
入队方法doAcquireSharedInterruptibly,先入同步队列,然后自旋判断 是否可以拿到许可值恢复线程执行并更新成头节点 或者 挂起
release()执行流程
拿到许可值的线程最终执行完finally 释放锁
semaphore.release()→ releaseShared() → 该方法通过 tryReleaseShared(arg)判断
tryReleaseShared()(CAS 增加 state)
返回ture 代表释放增加了当前可用许可值
没有false的返回,如果释放过程出错 抛出错误Error
然后执行doReleaseShared()方: 唤醒等待队列中的线程
该方法通过自旋方式 从头节点唤醒标识 开始唤醒下一个节点,unparkSuccessor(head.next);从而恢复上面那些没拿到许可值进入同步队列被挂起的线程,从而继续执行 这样就闭环喽
闭环的关键点
doAcquireSharedInterruptibly方法里的parkAndCheckInterrupt() 里挂起当前线程
doReleaseShared 方法里unparkSuccessor(h) 唤醒后续节点的线程解挂
Semaphore类使用注意事项
示例:控制最多 10 个线程并发访问资源
Semaphore semaphore = new Semaphore(10, true); // 公平模式
void accessResource() throws InterruptedException {
semaphore.acquire();
try {
// 访问共享资源
} finally {
semaphore.release();
}
}
- 避免死锁:确保 release() 在 finally 块中调用。
- 许可数量管理:不要过度释放(release() 次数超过 acquire())
- 性能权衡:非公平模式适用于高吞吐场景,公平模式适用于资源竞争激烈且需公平性的场景