Semaphore 有啥用?我用 3 个场景 + 源码分析
本文介绍了并发编程工具Semaphore的核心作用、实现原理及使用注意事项。Semaphore通过信号量计数器控制并发线程数量,适用于接口限流、连接池管理、生产者-消费者模型等场景。其底层基于AQS实现,通过state变量管理计数器,acquire()和release()方法分别控制信号量的获取与释放。文章特别强调要在finally块中释放信号量,区分公平锁与非公平锁的适用场景,并澄清Semaph
文章目录
前言
大家好,我是程序员梁白开,今天我们聊一聊Semaphore。
在并发编程领域,synchronized和Lock是大家耳熟能详的同步工具,但面对 “如何控制同时访问资源的线程数量” 这类问题,它们就显得有些力不从心。而Semaphore(信号量) 正是解决这类问题的 “一把好手”,今天咱们就从实际场景出发,扒透它的用法和底层原理,让你下次面试再遇到相关问题,能做到对答如流。
一、先搞懂:Semaphore 到底能解决什么问题?
先别急着看源码,咱们先搞清楚 Semaphore 的核心作用 ——控制并发线程的 “流量”,本质是通过一个 “信号量计数器”,限制同时访问特定资源的线程数量。我举 3 个真实场景,你立马就能明白它的价值。
场景 1:限制接口的并发请求数
假设你开发了一个查询用户订单的接口,后端数据库扛不住太多并发查询,需要限制同时只有 10 个线程能执行查询逻辑。这时候用 Semaphore 就能轻松实现:
// 初始化Semaphore,允许10个线程同时访问
private final Semaphore semaphore = new Semaphore(10);
public List<Order> queryUserOrders(Long userId) throws InterruptedException {
// 1. 尝试获取“信号量”,获取不到会阻塞
semaphore.acquire();
try {
// 2. 执行数据库查询(核心业务逻辑)
return orderMapper.selectByUserId(userId);
} finally {
// 3. 释放“信号量”,让其他线程能获取
semaphore.release();
}
}
这里的核心是:acquire()会让信号量计数器 - 1,release()会让计数器 + 1;当计数器为 0 时,后续调用acquire()的线程会被阻塞,直到有线程释放信号量。
场景 2:实现 “连接池” 的并发控制
我们常用的数据库连接池(如 HikariCP),本质上也用到了类似 Semaphore 的逻辑 —— 限制同时活跃的数据库连接数。比如连接池有 5 个连接,就相当于 Semaphore 的计数器初始化为 5:
- 线程获取连接时,先获取 Semaphore 的信号量;
- 线程释放连接时,再释放信号量;
- 当 5 个信号量都被获取,新线程需要等待其他线程释放连接(释放信号量)。
场景 3:解决 “生产者 - 消费者” 的容量限制
如果生产者生产速度太快,消费者消费速度太慢,容易导致中间缓冲区溢出。用 Semaphore 可以分别控制 “缓冲区的空闲位置数” 和 “缓冲区的已用位置数”:
- 生产者线程:需要获取 “空闲位置信号量” 才能生产(确保有地方存);
- 消费者线程:需要获取 “已用位置信号量” 才能消费(确保有数据可拿);
- 这样就能避免缓冲区溢出,实现生产和消费的平衡。
二、扒原理:Semaphore 是怎么实现 “流量控制” 的?
搞懂了用法,咱们再深入 JDK 源码(基于 JDK 1.8),看看 Semaphore 的底层逻辑。其实它的核心依赖AQS(AbstractQueuedSynchronizer) 实现,关键就在于 “信号量计数器” 的管理和线程的阻塞 / 唤醒。
1. 核心属性:两个内部类 + 一个 AQS 同步器
先看 Semaphore 的类结构,它有两个核心内部类,分别对应 “公平锁” 和 “非公平锁” 的实现,本质都是 AQS 的子类:
public class Semaphore implements java.io.Serializable {
// 同步器,核心是AQS的子类
private final Sync sync;
// 非公平锁实现(默认)
static final class NonfairSync extends Sync { ... }
// 公平锁实现
static final class FairSync extends Sync { ... }
// 抽象同步器,继承AQS
abstract static class Sync extends AbstractQueuedSynchronizer { ... }
}
而 AQS 中有一个state 变量,正是 Semaphore 的 “信号量计数器”—— 初始化 Semaphore 时传入的数字,其实就是给 AQS 的 state 赋值。比如new Semaphore(10),就相当于state = 10。
2. 关键方法 1:acquire ()—— 获取信号量
当线程调用acquire()时,本质是尝试减少 AQS 的 state 值,逻辑如下:
- 尝试将 state 减 1,如果减完后 state >= 0,说明获取成功,直接返回执行业务逻辑;
- 如果减完后 state <0,说明信号量已用完,当前线程会被封装成 “节点”,加入 AQS 的阻塞队列;
- 线程进入阻塞状态,等待其他线程释放信号量后被唤醒。
这里要注意 “公平锁” 和 “非公平锁” 的区别:
- 非公平锁(默认):线程获取信号量时,会先 “插队” 尝试获取,失败了再进阻塞队列;
- 公平锁:线程会直接进入阻塞队列,按 “先来后到” 的顺序获取信号量,不会插队。
3. 关键方法 2:release ()—— 释放信号量
当线程调用release()时,本质是增加 AQS 的 state 值,逻辑更简单:
- 将 state 加 1,更新 AQS 的 state 变量;
- 检查阻塞队列中是否有等待的线程;
- 如果有,唤醒队列头部的线程,让它重新尝试获取信号量(也就是重新执行 acquire () 的逻辑)。
一句话总结原理
Semaphore 通过AQS 的 state 变量管理信号量计数器,用acquire()减计数器实现 “限流”,用release()加计数器实现 “释放名额”,同时通过 AQS 的阻塞队列管理等待线程,最终实现并发线程的数量控制。
三、避坑指南:使用 Semaphore 必须注意的 3 个点
1.必须在 finally 中释放信号量
如果线程在获取信号量后执行业务逻辑时抛出异常,会导致release()无法执行,信号量被永久占用,最终导致其他线程一直阻塞。所以一定要在finally块中调用release(),确保信号量能正常释放。
2.区分公平锁和非公平锁的适用场景
- 追求性能用非公平锁(默认):插队机制能减少线程切换开销,效率更高;
- 要求顺序用公平锁:比如秒杀场景需要按请求顺序处理,避免 “插队” 导致的不公平问题,但性能会稍差。
3.别混淆 Semaphore 和 Lock 的区别
- Lock是 “互斥锁”,本质是控制 “资源的独占性”,同一时间只有 1 个线程能获取锁;
- Semaphore是 “信号量”,控制 “资源的并发访问数量”,同一时间可以有 N 个线程获取信号量(N 是初始化时的数字)。
四、面试真题:这些问题你能答上来吗?
最后给大家整理几个高频面试题,检验一下学习成果:
- Semaphore 和 ReentrantLock 的区别是什么?(核心答 “互斥” vs “限流”)
- Semaphore 的公平锁和非公平锁怎么实现的?(答 AQS 的队列机制和插队逻辑)
- 用 Semaphore 怎么实现一个简单的连接池?(结合场景 2 的逻辑,控制连接的获取和释放)
- 如果线程调用 acquire () 后,没调用 release () 会发生什么?(答信号量泄漏,其他线程阻塞)
如果觉得这篇文章对你有帮助,欢迎点赞 + 收藏,也可以在评论区分享你用 Semaphore 解决过的实际问题~ 后续我还会更新更多并发编程的干货,关注我不迷路!
更多推荐



所有评论(0)