《Semaphore 信号量机制:限流与并发控制的经典实现》


一、前言:为什么要用 Semaphore

在高并发场景下,我们常常需要控制同一时间允许访问某个资源的线程数量
比如:

  • 同时最多 10 个线程访问数据库连接池;
  • 同时最多 3 个线程下载文件;
  • 请求接口做限流控制。

这些场景都可以用 Semaphore(信号量) 来实现。

Semaphore 通过“许可证(permit)”机制控制并发数量,
线程在执行前必须先获取许可证,执行后释放许可证。

它是 JUC 中基于 AQS(AbstractQueuedSynchronizer) 实现的同步工具,
用途类似“计数器型锁”,但比锁更灵活。


二、核心概念与方法介绍

(1)构造方法
Semaphore semaphore = new Semaphore(int permits);
Semaphore semaphore = new Semaphore(int permits, boolean fair);
参数 含义
permits 可用许可证数量
fair 是否使用公平模式(FIFO)
(2)核心方法
方法 说明
acquire() 获取一个许可,若无则阻塞
acquire(int n) 获取多个许可
tryAcquire() 尝试获取,不阻塞
tryAcquire(long timeout, TimeUnit unit) 超时获取
release() 释放一个许可
release(int n) 释放多个许可
availablePermits() 返回当前可用许可数
drainPermits() 一次性清空所有许可

简而言之:

acquire() → 占用资源;release() → 释放资源。


三、底层实现原理(AQS 支撑)

(1)内部类结构

Semaphore 内部通过继承 AQS 实现两种模式:

static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}

两者的差异仅在于获取许可时是否遵循 FIFO 排队。

(2)核心字段
private final Sync sync; // 继承自 AQS

AQS 的 state 变量记录当前可用许可证数量:

state = permits
(3)获取许可(acquire)逻辑

简化源码:

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

逻辑说明:

  1. 获取当前剩余许可数;
  2. 若不足,则阻塞当前线程;
  3. 若足够,则用 CAS 减少许可数并立即返回。
(4)释放许可(release)逻辑
protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        int current = getState();
        int next = current + releases;
        if (compareAndSetState(current, next))
            return true;
    }
}

释放操作会唤醒等待队列中的线程继续获取许可。


四、公平与非公平模式

模式 说明 特点
非公平模式(默认) 线程到达后立即尝试获取许可,不管是否排队 吞吐量高,但可能“插队”
公平模式 严格按照等待顺序(FIFO) 公平但性能略低

源码对比:

// 非公平
if (compareAndSetState(available, remaining)) return remaining;

// 公平
if (hasQueuedPredecessors()) return -1;

在限流系统中,公平模式可以避免“饿死”线程;
而在性能优先场景中,非公平模式更常用。


五、典型应用场景与实战示例

(1)限流控制

限制同时执行的线程数量:

Semaphore semaphore = new Semaphore(3);

for (int i = 0; i < 10; i++) {
    new Thread(() -> {
        try {
            semaphore.acquire(); // 获取许可
            System.out.println(Thread.currentThread().getName() + " 执行任务中");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release(); // 释放许可
        }
    }).start();
}

输出:

Thread-1 执行任务中
Thread-2 执行任务中
Thread-3 执行任务中
(1秒后)
Thread-4 执行任务中 ...

任意时刻只有 3 个线程在执行任务。

(2)数据库连接池控制

在多线程访问数据库连接池时,可以用 Semaphore 控制连接数量:

Semaphore dbSemaphore = new Semaphore(10);

确保连接不被过度占用,避免数据库过载。

(3)流量限速(接口级别)

每秒允许 N 个请求通过,可在服务网关层用信号量实现动态限流。

(4)多线程同步启动

在并发压测中可通过 Semaphore(0) 控制所有线程同步开始:

Semaphore start = new Semaphore(0);
for (...) {
    new Thread(() -> {
        try {
            start.acquire(); // 等待统一放行
            task();
        } catch (Exception e) {}
    }).start();
}
Thread.sleep(1000);
start.release(threadCount); // 所有线程同时开始

六、面试高频问题与答题模板

问题 答案要点
Q1:Semaphore 是如何实现的? 基于 AQS 的共享模式,state 表示剩余许可数,通过 CAS 控制并发。
Q2:Semaphore 与 Lock 的区别? Lock 是独占锁,只允许一个线程;Semaphore 控制多个线程共享资源。
Q3:Semaphore 的公平模式如何实现? 通过判断 hasQueuedPredecessors() 确保 FIFO 顺序。
Q4:acquire() 与 tryAcquire() 区别? 前者会阻塞,后者立即返回布尔结果。
Q5:可以动态增加许可证吗? 可以通过 release(int n) 增加许可数。
Q6:Semaphore 是否可重入? 不可重入,一个线程获取许可后需显式释放。
Q7:Semaphore 与 CountDownLatch 区别? Semaphore 控制“可用数量”,CountDownLatch 用于“等待倒计时”。

结语

Semaphore 是并发编程中最经典的限流工具之一,
它通过 AQS + 许可证机制 优雅地实现了“并发访问数量控制”。

无论是线程池控制、接口限流、资源配额
Semaphore 都能用最小代价实现高效安全的并发管理。

下一篇,我将写——
《ThreadLocal 深入解析:线程副本机制与内存泄漏风险》

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐