找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3464

积分

0

好友

474

主题
发表于 2026-2-12 08:58:35 | 查看: 31| 回复: 0

在 JUC 并发工具包中,我们不仅需要保证资源的互斥访问,在很多高并发场景下,更重要的是控制同一时刻能够访问特定资源的线程数量。无论是接口限流、数据库连接池管理,还是并发任务数管控,其底层实现都绕不开一个核心机制——AQS 的共享模式

它正是 Semaphore(信号量)、CountDownLatch(倒计时门闩)以及 CyclicBarrier(循环屏障)等经典工具的共同基石

一、一句话分清 AQS 独占模式与共享模式

理解这两种模式是掌握 JUC 底层的关键。

  • 独占模式(Exclusive):资源只有 1 份,同一时间只能被 1 个线程持有。ReentrantLock 是其代表。
  • 共享模式(Shared):资源有 N 份,同一时间可以允许多个线程同时持有。SemaphoreCountDownLatch 是典型实现。

本质区别就在于资源数量的定义和分配方式。

二、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 共享模式核心源码解析

理解了基本思想,我们深入到 acquireSharedreleaseShared 这两个核心方法的实现中。

1. 核心入口:acquireShared(int arg)

这是获取共享资源的顶层入口。

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0) {
        // 获取失败 → 进入队列等待
        doAcquireShared(arg);
    }
}

这里有两个关键点:

  1. tryAcquireShared(arg) 是一个由子类实现的钩子方法(例如 Semaphore 会定义自己的实现逻辑)。其返回值语义明确:
    • 返回值 ≥ 0:表示获取成功。
    • 返回值 < 0:表示获取失败,当前线程需要进入同步队列阻塞等待。
  2. 如果获取失败,则调用 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; // 返回剩余资源数
    }
}

逻辑清晰明了:

  1. 获取当前可用许可证数量 (state)。
  2. 计算获取后的剩余数量 (remaining = state - acquires,通常 acquires 为1)。
  3. 如果 remaining < 0,说明资源不足,直接返回负数,获取失败。
  4. 否则,尝试用 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应用的关键。更多关于JUCAQS源码解析与实战技巧,可以在技术社区持续交流探讨。

六、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 中公平与非公平策略的具体实现,我们看到了一个优雅而强大的并发控制模型。

理解了共享模式,CountDownLatchCyclicBarrier 的原理也呼之欲出。在并发编程中,除了同步互斥,这种基于数量的协同同样至关重要。


下一篇预告:我们将继续深入 AQS,解析另一个重要机制——条件队列(Condition Queue)Condition 接口如何与 Lock 配合实现精准的线程等待/通知?其底层与同步队列有何关联?这将是我们彻底吃透 AQS 的最后一站。




上一篇:Meta策略拍卖框架SALE解析:如何用AI拍卖机制降低42%的Agent API成本
下一篇:我用AI渗透测试工具Shannon实测了自己的项目,发现了什么?
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 14:19 , Processed in 0.678881 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表