一、实现原理
Semaphore 基于 AQS(AbstractQueuedSynchronizer) + CAS 实现,可通过构造方法传入布尔参数,选择公平锁或非公平锁,默认采用非公平锁,控制同一时间内访问共享资源的最大线程数。
⚠️注意:
✔️acquire 方法用于向 Semaphore 申请获取许可证,该方法具有阻塞特性
✔️tryAcquire 方法用于尝试向 Semaphore 获取许可证,核心为非阻塞逻辑
✔️信号量赋值AbstractQueuedSynchronizer(AQS)的state属性值
二,使用场景
Semaphore 的典型应用场景,主要集中在需要限制资源访问数量或控制并发访问的场景,例如数据库连接池、文件读写、网络请求等。在这类场景下,Semaphore 能够有效协调多线程对共享资源的访问,从而保障系统的稳定性与整体性能。
1、🌰限流场景
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
/**
* 模拟限流场景
*/
@Slf4j
public class SemaphoreDemo {
/**
* 同一时刻最多只允许有两个并发
*/
private static Semaphore semaphore = new Semaphore(2);
/**
* 线程池,模拟并发
*/
private static Executor executor = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
executor.execute(() -> getInfo());
}
}
public static void getInfo() {
if (!semaphore.tryAcquire()) {
log.error("被流控了");
// TODO: 熔断、限流等
return;
}
try {
log.info("获取信息成功");
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
semaphore.release();
}
}
}
2、🌰输出
14:51:14.490 [pool-1-thread-8] ERROR com.sync.SemaphoreDemo - 被流控了
14:51:14.490 [pool-1-thread-4] ERROR com.sync.SemaphoreDemo - 被流控了
14:51:14.490 [pool-1-thread-1] INFO com.sync.SemaphoreDemo - 获取信息成功
14:51:14.490 [pool-1-thread-10] ERROR com.sync.SemaphoreDemo - 被流控了
14:51:14.490 [pool-1-thread-3] ERROR com.sync.SemaphoreDemo - 被流控了
14:51:14.490 [pool-1-thread-6] ERROR com.sync.SemaphoreDemo - 被流控了
14:51:14.490 [pool-1-thread-9] ERROR com.sync.SemaphoreDemo - 被流控了
14:51:14.490 [pool-1-thread-2] INFO com.sync.SemaphoreDemo - 获取信息成功
14:51:14.490 [pool-1-thread-5] ERROR com.sync.SemaphoreDemo - 被流控了
14:51:14.490 [pool-1-thread-7] ERROR com.sync.SemaphoreDemo - 被流控了
三 、 构造函数
Semaphore 可通过构造方法传入布尔参数,可以选择公平锁或非公平锁,默认采用非公平锁。
1、代码
// AQS的实现
private final Sync sync;
// 默认非公平锁
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
// 根据布尔值选择使用公平锁还是非公平锁
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
// 信号量就是AbstractQueuedSynchronizer的state属性值
Sync(int permits) {
setState(permits);
}
⚠️注意:
✔️信号量赋值到AbstractQueuedSynchronizer的state属性值
四、常用方法
1、acquire
若当前无可用许可证(AbstractQueuedSynchronizer的state可用数量不足),调用该方法的线程会进入阻塞状态,且会持续等待,直至成功获取到所需的许可证(或线程被中断)
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
//调用 tryAcquireShared(arg) 尝试以共享模式获取锁。
//AbstractQueuedSynchronizer的state,可用信号量小于0,进入阻塞队列
if (tryAcquireShared(arg) < 0)
//调用 doAcquireSharedInterruptibly(arg) 方法,将当前线程加入等待队列并阻塞,直到获取锁或被中断。
doAcquireSharedInterruptibly(arg);
}
// 当前线程加入等待队列并阻塞,直到获取锁或被中断
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
//是否轮到当前节点获取锁
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// shouldParkAfterFailedAcquire 判断是否需要将当前线程挂起
// parkAndCheckInterrupt注释当前线程,同时返回当前线程中断状态,如果true
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
1.1、非公平:NonfairSync
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
//获取AbstractQueuedSynchronizer的state,可用信号量
int available = getState();
int remaining = available - acquires;
//可用信号量大于0
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
1.2、公平锁:FairSync
protected int tryAcquireShared(int acquires) {
for (;;) {
// 判断队列中是否有数据,如果有,直接返回-1,小于0,直接同步队列
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
⚠️注意:
✔️阻塞状态会进入CLH队列
✔️公平锁与非公平锁的核心区别:
公平锁:若同步队列中已有等待线程,当前线程会直接进入队列排队,严格按照 FIFO 顺序获取锁,保证先来先服务。
非公平锁:当前线程会先直接通过 CAS 操作尝试抢占锁,只有抢占失败时,才会进入同步队列排队。
2、tryAcquire
核心为非阻塞逻辑
- tryAcquire:若可用许可证数量少于申请数量,线程立即返回 false
- tryAcquire (long timeout, TimeUnit unit):带超时参数,则会在指定时间内等待许可证,超时仍未获取则返回 false,成功获取则返回 true
public boolean tryAcquire() {
// 默认使用了非公平
return sync.nonfairTryAcquireShared(1) >= 0;
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
Semaphore.Sync.nonfairTryAcquireShared代码如下
//非公平方法
final int nonfairTryAcquireShared(int acquires) {
// 自旋
for (;;) {
//获取AbstractQueuedSynchronizer的state,可用信号量
//信号量小于0,直接返回
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
⚠️注意:
✔️tryAcquire非阻塞逻辑会直接返回,不进入CLH队列
✔️当前线程会先直接通过 CAS 操作尝试抢占锁,抢占失败时,直接返回false
3、release
释放一个许可证,使可用许可证的数量增加,如果一个线程正在尝试获取许可,那么就有一个线程,被选中并获得了刚刚发布的许可证
public void release() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
//将当前状态值 current 加上要释放的资源数 releases,得到新的状态值 next
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
// CAS原子操作,只有当当前状态仍为 current 时,才会将其更新为 next。
if (compareAndSetState(current, next))
return true;
}
}
private void doReleaseShared() {
for (;;) {
Node h = head;
// 判断头节点 h 是否不为空且不是尾节点 tail
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果头节点的等待状态为 Node.SIGNAL
if (ws == Node.SIGNAL) {
//使用 CAS 将其状态从 Node.SIGNAL 设置为 0,如果 CAS 失败,则继续循环重新检查。
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
//成功后调用 unparkSuccessor(h) 唤醒后继节点。
unparkSuccessor(h);
}
//如果头节点的等待状态为 0:
//使用 CAS 将其状态设置为 Node.PROPAGATE。
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
//如果头节点未变,则跳出循环。
//如果头节点已变,说明有其他线程修改了队列结构,需继续循环处理
if (h == head)
break;
}
}
⚠️注意:
✔️Semaphore 的 release 方法本身不包含显式的队列出队逻辑。
✔️队列节点的出队操作采用延迟执行的策略:
该操作并非在释放许可时触发,而是发生在被唤醒的等待线程成功获取许可之后
线程在acquire()方法调用的doAcquireSharedInterruptibly()中被阻塞,被唤醒后成功抢占到许可,线程最终通过setHead()方法完成节点出队(将当前节点设为队列头,并移除原头节点的引用)。
五、总结
Semaphore(信号量)是高并发场景下十分实用的并发工具类,其核心作用是控制同一时间内访问共享资源的最大线程数。常见应用场景如下:
- 接口限流:通过限制并发访问量,对共享资源的请求流量进行控制,保护系统稳定。
- 资源池管理:可用于实现数据库连接池、线程池、连接池等有限资源池,统一管理一组稀缺共享资源的分配与释放。