Semaphore 超详细完整版讲解

类路径:java.util.concurrent.Semaphore 中文名称:信号量

一、核心概念

通俗理解

停车场模型: 信号量内部维护固定数量停车位(许可 permit)

  • acquire():开车进场,拿走 1 个许可,没有车位就阻塞等待
  • release():车辆离场,归还 1 个许可

本质:控制同一时间能够执行的线程数量,实现资源限流、资源抢占。

两大模式:

  1. 共享锁思想(底层基于 AQS 共享模式),多个线程可以同时获取许可
  2. 分为:公平信号量 / 非公平信号量

对比区分记忆

  • CountDownLatch:等待任务完成(门闩)
  • CyclicBarrier:线程互相等待集合(栅栏)
  • Semaphore:控制并发数量(通行证)

二、全部构造方法

// permits:初始许可数量,非公平模式
public Semaphore(int permits)

// fair=true:公平信号量;false:非公平
public Semaphore(int permits, boolean fair)

  

  • permits ≥ 0;permits=0 初始无许可,必须外部 release 才能获取
  • 非公平(默认):线程释放许可后,新来线程和等待队列线程竞争许可,性能更高
  • 公平模式:严格按照等待队列顺序获取,先等先得,上下文切换多,吞吐量略低

三、完整 API 分类

1. 阻塞获取许可(常用)

// 获取1个许可,无许可则阻塞;可被中断
void acquire() throws InterruptedException

// 获取N个许可
void acquire(int permits) throws InterruptedException

  

2. 不响应中断的阻塞获取(极少用)

void acquireUninterruptibly()
void acquireUninterruptibly(int permits)

  

线程被 interrupt 时不会抛出异常,会继续等待许可,业务尽量避免。

3. 非阻塞尝试获取(推荐生产使用,防止死锁)

// 立刻尝试,拿到返回true,没有直接false,不阻塞
boolean tryAcquire()
boolean tryAcquire(int permits)

// 带超时尝试获取
boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException
boolean tryAcquire(int permits, long timeout, TimeUnit unit)

  

4. 释放许可

// 归还1个许可
void release()
// 归还N个许可
void release(int permits)

  

⚠️ 重大注意:release 不会校验是不是当前线程 acquire 的!随便哪个线程都能释放,容易出现许可膨胀!

5. 监控工具方法(瞬时快照,不能用于业务判断)

int availablePermits()      // 当前剩余可用许可
int drainPermits()          // 一次性拿走所有可用许可,返回拿到数量
int getQueueLength()         // 获取正在等待许可的线程预估数量
boolean hasQueuedThreads()   // 是否存在等待线程
boolean isFair()             // 是否公平模式

  

四、底层 AQS 源码核心逻辑

Semaphore 内部同样有 Sync,继承 AQS:

  • state:代表当前剩余许可数量

注意和 CountDownLatch 区分: CountDownLatch:state = 剩余需要 countDown 的次数 Semaphore:state = 当前可用通行证

非公平模式核心方法

// 获取许可共享尝试
protected int tryAcquireShared(int acquires) {
    for (;;) {
        int available = getState();
        int remaining = available - acquires;
        // 许可不足 返回-1 进入阻塞队列
        if (remaining < 0 || compareAndSetState(available, remaining))
            return remaining;
    }
}

// 释放许可
protected 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))
            return true;
    }
}

  

流程梳理:

  1. acquire()tryAcquireShared()
    • state ≥ 需要许可:CAS 扣减 state,直接执行业务
    • state 不足:线程进入 AQS 共享阻塞队列挂起
  2. release()tryReleaseShared()
    • CAS 增加 state;成功后唤醒队列头部等待线程

关键点: Semaphore 允许多次 release,state 可以超过初始 permits! 也就是:许可上限不受初始化数值约束,人为释放过多会造成「许可膨胀」,限流失效。

五、基础代码示例

示例 1:简单限流,最多 3 个线程同时执行

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreDemo {
    public static void main(String[] args) {
        // 3个许可:同一时刻最多3条线程运行
        Semaphore semaphore = new Semaphore(3);

        // 模拟10个并发请求
        for (int i = 1; i <= 10; i++) {
            int taskId = i;
            new Thread(() -> {
                try {
                    // 获取通行证
                    semaphore.acquire();
                    System.out.println("任务" + taskId + "获取许可,开始执行");
                    TimeUnit.SECONDS.sleep(2);
                    System.out.println("任务" + taskId + "执行完成,释放许可");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // finally释放,保证一定会归还许可
                    semaphore.release();
                }
            }).start();
        }
    }
}

  

运行现象:始终最多 3 个任务同时执行。

示例 2:tryAcquire 超时避免永久阻塞(生产标准写法)

Semaphore semaphore = new Semaphore(2);

new Thread(() -> {
    boolean acquire = false;
    try {
        // 最多等待1秒拿许可
        acquire = semaphore.tryAcquire(1, TimeUnit.SECONDS);
        if (acquire) {
            System.out.println("获取许可执行业务");
            TimeUnit.SECONDS.sleep(1);
        } else {
            // 限流兜底策略:直接返回繁忙、降级
            System.out.println("获取许可超时,触发限流降级");
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        if (acquire) {
            semaphore.release();
        }
    }
}).start();

  

✅ 最佳实践:一定要标记是否成功获取许可,只有拿到许可才 release!

六、典型业务使用场景

场景 1:接口 / 资源并发限流(最常用)

限制访问有限资源的并发量:

  • 第三方 HTTP 接口调用限流(防止调用方被对方风控)
  • 文件读写、FTP 连接、数据库外置连接控制

和线程池区别: 线程池限制线程数量;队列还能堆积任务; Semaphore 限制业务并发数量,和线程池无关,可以跨线程池限流。

场景 2:资源池简易实现

连接池、对象池:许可代表可用资源,acquire 拿资源,release 归还资源。

场景 3:实现读写锁(扩展思路)

可以用 Semaphore 简单模拟 ReadWriteLock:

  • 读:获取少量许可,支持并发读
  • 写:一次性获取全部许可,排斥其他读写(性能不如 ReentrantReadWriteLock,仅思路)

场景 4:异步任务的批量并发控制

批量同步调用外部服务,控制并发数,防止瞬间打垮下游。

场景 5:实现屏障(拓展:permits=1 可当作互斥锁)

new Semaphore(1) 等价简易独占锁,功能类似 ReentrantLock,但不可重入!

致命坑:同一个线程连续 acquire (1) 两次直接死锁,不要当做锁使用!

七、高频致命坑(面试重点)

坑 1:acquire /release 不配对,许可膨胀

// 危险!没有判断是否拿到许可就直接release
try {
    semaphore.tryAcquire(1, TimeUnit.SECONDS);
} finally {
    semaphore.release(); 
}

  

如果获取失败依然执行 release → state 持续上涨,限流彻底失效。 ✅ 规范:用布尔变量标记,只有 acquire 成功才释放。

坑 2:同一个线程多次 acquire,忘记对应次数 release

acquire(2) 必须 release(2),少释放会导致许可持续泄漏。

坑 3:Semaphore (1) 当成可重入锁使用

不可重入!

sem.acquire();
sem.acquire(); // 同一个线程再次获取 → 永久阻塞死锁

  

需要重入锁使用 ReentrantLock。

坑 4:生产直接使用 acquire () 无限阻塞

下游持续占用许可不释放 → 线程全部阻塞,服务雪崩; 强制规范:业务代码优先 tryAcquire(time,unit),设计降级策略。

坑 5:随意混用 acquire (N)、release (N)

尽量统一使用 acquire () /release () 单次操作,便于排查许可泄漏问题。

八、公平 vs 非公平信号量怎么选?

  1. 默认非公平(推荐绝大多数业务)
    • 性能高,减少线程上下文切换
    • 缺点:可能出现线程饥饿(新来线程抢占许可,老线程长期等待)
  2. 公平模式
    • 严格 FIFO 顺序获取许可
    • 适合对等待顺序有强要求场景,并发量大吞吐量下降

九、面试高频对比:Semaphore vs 另外两个工具

 

工具核心作用底层 AQS 模式
CountDownLatch 等待一组任务全部完成 共享锁
CyclicBarrier 线程互相等待集合,循环复用 ReentrantLock+Condition
Semaphore 控制并发数量、资源限流 共享锁

面试官经典追问:

Q:线程池 maximumPoolSize 限流 和 Semaphore 限流区别? A:

  1. ThreadPoolExecutor:限制工作线程数量,任务存在队列堆积;适合内部任务调度。
  2. Semaphore:限制同时运行的业务并发数,与线程池无关;适合保护外部第三方资源(调用外部接口、文件、中间件),可以快速拒绝请求实现限流降级。

Q:Semaphore 可以实现 CountDownLatch 吗? A:语法可以模拟,但语义不匹配,不推荐。

十、一句话面试背诵总结

Semaphore 是基于 AQS 共享模式的信号量,通过许可数量控制同一时间并发访问资源的线程数,常用来做限流;支持公平 / 非公平模式;开发务必保证 acquire 和 release 成对,优先使用带超时 tryAcquire,防止许可泄漏与线程永久阻塞。

拓展:三者完整区分终极面试话术

  • CountDownLatch:老板等员工干完活,一次性
  • CyclicBarrier:员工互相等待集合,集齐出发,可循环
  • Semaphore:停车场通行证,控制同时进场人数,限流

 




微信图片_20260323111738_59_204

 

posted @ 2026-07-23 16:18  程序员食堂  阅读(13)  评论(0)    收藏  举报