在 JUC 并发工具包中,我们不仅需要保证资源的互斥访问,在很多高并发场景下,更重要的是控制同一时刻能够访问特定资源的线程数量。无论是接口限流、数据库连接池管理,还是并发任务数管控,其底层实现都绕不开一个核心机制——AQS 的共享模式。
它正是 Semaphore(信号量)、CountDownLatch(倒计时门闩)以及 CyclicBarrier(循环屏障)等经典工具的共同基石。
一、一句话分清 AQS 独占模式与共享模式
理解这两种模式是掌握 JUC 底层的关键。
- 独占模式(Exclusive):资源只有 1 份,同一时间只能被 1 个线程持有。
ReentrantLock 是其代表。
- 共享模式(Shared):资源有 N 份,同一时间可以允许多个线程同时持有。
Semaphore 和 CountDownLatch 是典型实现。
本质区别就在于资源数量的定义和分配方式。
二、AQS 共享模式核心结构
我们先看看 AbstractQueuedSynchronizer 中与共享模式相关的基础字段:
public abstract class AbstractQueuedSynchronizer {
// 共享状态:多个线程可以同时持有
private volatile int state;
// CLH 同步队列
private transient volatile Node head;
private transient volatile Node tail;
}
共享模式的核心思想非常直观:
state 代表可用的资源总数。
- 线程获取资源时,尝试将
state 值减 1。
- 线程释放资源时,将
state 值加 1。
- 只要
state > 0,就允许新的线程获取资源。
三、AQS 共享模式核心源码解析
理解了基本思想,我们深入到 acquireShared 和 releaseShared 这两个核心方法的实现中。
1. 核心入口:acquireShared(int arg)
这是获取共享资源的顶层入口。
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) {
// 获取失败 → 进入队列等待
doAcquireShared(arg);
}
}
这里有两个关键点:
tryAcquireShared(arg) 是一个由子类实现的钩子方法(例如 Semaphore 会定义自己的实现逻辑)。其返回值语义明确:
- 返回值 ≥ 0:表示获取成功。
- 返回值 < 0:表示获取失败,当前线程需要进入同步队列阻塞等待。
- 如果获取失败,则调用
doAcquireShared(arg) 方法将线程加入队列。
2. 核心方法:doAcquireShared —— 入队与等待
这是共享模式下线程入队和阻塞的核心逻辑。
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED); // 1. 以共享模式创建节点并入队
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { // 自旋
final Node p = node.predecessor();
if (p == head) { // 2. 如果前驱节点是头节点,说明轮到自己尝试获取了
int r = tryAcquireShared(arg); // 再次尝试获取
if (r >= 0) { // 获取成功
// 3. 设置自己为头节点,并尝试唤醒后续的共享节点(传播)
setHeadAndPropagate(node, r);
p.next = null; // 帮助GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 4. 判断是否需要阻塞(前驱节点状态为SIGNAL),然后阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
共享模式的最大特点就体现在 setHeadAndPropagate(node, r) 这个方法名上——传播(propagate)。当一个共享节点被唤醒并成功获取资源后,它可能会连续唤醒队列中后面所有正在等待的共享节点,从而实现“一批线程同时被放行”的效果,这与独占模式每次只唤醒一个线程截然不同。
3. 释放锁:releaseShared
释放共享资源的入口同样清晰。
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { // 1. 尝试释放资源(子类实现)
// 2. 释放成功 → 唤醒队列中的后继节点
doReleaseShared();
return true;
}
return false;
}
这里的 doReleaseShared() 方法是 AQS 共享模式中确保正确、高效唤醒的关键。它内部通过循环和 CAS 操作,确保在并发释放的场景下,所有等待的共享线程都能被可靠地唤醒,完美支撑了“传播唤醒”的语义。
四、Semaphore 源码解析:AQS 共享模式的经典应用
Semaphore(信号量)是 AQS 共享模式最直观的体现,它的作用就是控制最多 N 个线程同时访问资源,是实现限流的核心工具。
1. Semaphore 内部结构
public class Semaphore {
private final Sync sync; // 所有操作都代理给这个同步器
abstract static class Sync extends AbstractQueuedSynchronizer {
Sync(int permits) {
setState(permits); // 初始化 state 为许可证数量
}
}
// 非公平同步器(默认)
static final class NonfairSync extends Sync {
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
// 公平同步器
static final class FairSync extends Sync {
protected int tryAcquireShared(int acquires) {
// 公平策略:先检查队列是否有等待者,有则排队
// ... (具体实现略)
}
}
}
可以看到,Semaphore 的核心就是一个继承自 AQS 的 Sync 内部类。构造时传入的许可数 permits 直接设定了 AQS state 的初始值。
2. 核心:非公平获取许可证
非公平模式是 Semaphore 的默认策略,其获取逻辑非常精炼。
final int nonfairTryAcquireShared(int acquires) {
for (;;) { // 自旋CAS
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || // 资源不足,直接返回负数
compareAndSetState(available, remaining)) // 资源充足,CAS更新状态
return remaining; // 返回剩余资源数
}
}
逻辑清晰明了:
- 获取当前可用许可证数量 (
state)。
- 计算获取后的剩余数量 (
remaining = state - acquires,通常 acquires 为1)。
- 如果
remaining < 0,说明资源不足,直接返回负数,获取失败。
- 否则,尝试用 CAS 将
state 更新为 remaining,成功则返回剩余数(>=0),表示获取成功。
这正是 tryAcquireShared 方法返回值语义的典型实现。
3. 释放许可证
释放操作同样通过自旋 CAS 保证线程安全。
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (compareAndSetState(current, next))
return true; // 释放成功,会触发 doReleaseShared 唤醒等待线程
}
}
释放就是将 state 值增加,成功后会调用 AQS 的 doReleaseShared() 方法,唤醒同步队列中等待的线程来获取新释放的资源。
五、公平信号量 vs 非公平信号量
选择哪种策略取决于你的具体场景:
- 非公平信号量(默认):线程获取许可证时,先直接尝试 CAS 抢锁,抢不到才乖乖排队。这种方式减少了线程挂起和唤醒的开销,吞吐量更高,但可能导致某些线程“饥饿”(长时间抢不到)。
- 公平信号量:线程获取许可证时,首先检查同步队列中是否有线程在等待。如果有,那么自己必须去队尾排队。这保证了先到的线程先获得服务,避免了饥饿,但性能开销稍大。
对于绝大多数业务场景,使用默认的非公平模式即可获得更好的性能。深入理解这些并发工具的底层,是构建高性能、高可靠Java应用的关键。更多关于JUC和AQS的源码解析与实战技巧,可以在技术社区持续交流探讨。
六、Semaphore 实战应用场景
掌握了原理,我们来看看 Semaphore 能直接落地的场景:
- 接口限流:控制某个接口或方法同一时刻的最大并发请求数,例如最多允许20个线程同时执行核心业务逻辑。
- 连接池限流:限制访问数据库、Redis等外部资源的连接数,防止连接耗尽。
- 任务流速控制:在生产者-消费者模式中,控制任务被处理的速度,避免瞬间压垮下游系统。
标准使用模板如下:
// 初始化一个包含20个许可证的信号量
Semaphore semaphore = new Semaphore(20);
try {
semaphore.acquire(); // 获取一个许可证,如果无可用则阻塞
// 执行受保护的业务逻辑...
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 处理中断
} finally {
semaphore.release(); // 无论如何,最终必须释放许可证
}
务必在 finally 块中释放许可证,这是避免许可证泄漏、导致系统假死的铁律。
七、知识体系串联
现在,我们可以将 JUC 的核心组件串联起来:
ReentrantLock → 基于 AQS 独占模式 实现的互斥锁。
Semaphore → 基于 AQS 共享模式 实现的资源计数器/限流器。
CountDownLatch → 同样是 AQS 共享模式 的一种应用,特点是 state 需要被减到 0 才会唤醒所有等待线程。
ThreadPoolExecutor 等线程池 → 其内部对工作线程状态的控制,也大量运用了 AQS 的状态管理思想。
打通了 AQS 的独占与共享模式,你就掌握了 JUC 并发包最核心的底层逻辑,面对其他同步工具时也能快速触类旁通。
总结与预告
本文我们深入剖析了 AQS 共享模式的工作原理 及其最典型的实现 Semaphore 信号量的源码。从 state 作为资源计数器的抽象,到 acquireShared/releaseShared 的获取与释放流程,再到 Semaphore 中公平与非公平策略的具体实现,我们看到了一个优雅而强大的并发控制模型。
理解了共享模式,CountDownLatch 和 CyclicBarrier 的原理也呼之欲出。在并发编程中,除了同步互斥,这种基于数量的协同同样至关重要。
下一篇预告:我们将继续深入 AQS,解析另一个重要机制——条件队列(Condition Queue)。Condition 接口如何与 Lock 配合实现精准的线程等待/通知?其底层与同步队列有何关联?这将是我们彻底吃透 AQS 的最后一站。