Java中的信号量(Semaphore)详解
字数 1907 2025-12-07 10:26:48
Java中的信号量(Semaphore)详解
一、知识点描述
信号量(Semaphore)是Java并发包(JUC)中用于控制并发访问资源数量的同步工具。它维护一组“许可证”(permits),线程访问资源前需获取许可证,访问后释放许可证,以此限制同时访问资源的线程数。信号量常用于流量控制、资源池管理、限流等场景,是解决多线程协同工作的重要工具之一。
二、核心原理与类结构
-
基本概念:
- 信号量内部维护一个整数计数器,表示可用许可证数量。
- 线程通过
acquire()获取许可证(计数器减1),若计数器为0则线程阻塞。 - 线程通过
release()释放许可证(计数器加1),唤醒等待线程。
-
类定义:
java.util.concurrent.Semaphore- 主要构造方法:
Semaphore(int permits) // 指定许可证数量,非公平模式 Semaphore(int permits, boolean fair) // 指定是否公平模式 - 公平模式:按线程阻塞顺序分配许可证;非公平模式允许插队,性能更高。
三、核心方法详解
-
获取许可证:
acquire():获取1个许可证,阻塞直到获取成功或被中断。acquire(int permits):获取指定数量许可证。tryAcquire():尝试获取,成功返回true,失败立即返回false。tryAcquire(long timeout, TimeUnit unit):带超时的尝试获取。
-
释放许可证:
release():释放1个许可证。release(int permits):释放指定数量许可证。
-
辅助方法:
availablePermits():返回当前可用许可证数。drainPermits():获取并返回所有立即可用许可证。isFair():判断是否为公平模式。
四、使用示例与场景分析
-
场景模拟:数据库连接池限流
public class ConnectionPool { private final Semaphore semaphore; private final List<Connection> connections = new ArrayList<>(); public ConnectionPool(int poolSize) { // 初始化许可证数量等于连接池大小 semaphore = new Semaphore(poolSize); for (int i = 0; i < poolSize; i++) { connections.add(createConnection()); } } public Connection getConnection() throws InterruptedException { semaphore.acquire(); // 获取许可证,若无可用则阻塞 return getAvailableConnection(); } public void releaseConnection(Connection conn) { returnConnection(conn); semaphore.release(); // 释放许可证 } } -
场景扩展:限流器(每秒钟最多10个请求)
public class RateLimiter { private final Semaphore semaphore = new Semaphore(10); private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); public RateLimiter() { // 每秒释放所有许可证 scheduler.scheduleAtFixedRate(() -> { int available = semaphore.availablePermits(); if (available < 10) { semaphore.release(10 - available); } }, 0, 1, TimeUnit.SECONDS); } public boolean tryRequest() { return semaphore.tryAcquire(); // 非阻塞尝试 } }
五、源码关键机制解析
-
内部实现:
- 基于AQS(AbstractQueuedSynchronizer)的共享模式实现。
- 许可证数量对应AQS的
state变量,获取时state减少,释放时state增加。
-
公平与非公平差异:
// 公平模式:检查是否有前驱节点在等待 protected int tryAcquireShared(int acquires) { if (hasQueuedPredecessors()) // 关键判断 return -1; // ... 修改state } // 非公平模式:直接尝试修改state -
释放操作的连锁反应:
release()会触发AQS的doReleaseShared(),唤醒等待队列中的后继节点。- 被唤醒的线程会重新尝试获取许可证,形成“连锁唤醒”效果。
六、注意事项与最佳实践
-
许可证数量设置:
- 初始值应基于系统负载和资源容量评估。
- 动态调整示例:
semaphore.drainPermits(); // 清空当前许可证 semaphore.release(newPermits); // 设置新数量
-
避免死锁:
- 获取和释放必须成对出现,建议使用try-finally:
semaphore.acquire(); try { // 访问共享资源 } finally { semaphore.release(); }
- 获取和释放必须成对出现,建议使用try-finally:
-
与CountDownLatch/CyclicBarrier的区别:
工具 特点 适用场景 Semaphore 控制并发数,许可证可重复使用 流量控制、资源池 CountDownLatch 一次性屏障,等待事件完成 启动准备、结束等待 CyclicBarrier 可重置屏障,线程相互等待 分阶段任务协同 -
性能优化建议:
- 非竞争场景下优先用非公平模式(默认)。
- 高竞争场景用公平模式避免线程饥饿。
- 使用
tryAcquire()设置合理超时,避免永久阻塞。
七、常见面试问题扩展
-
Q:信号量初始化为1时,与ReentrantLock有何区别?
- A:此时为二进制信号量(Binary Semaphore),但信号量无“持有线程”概念,任何线程都可调用
release(),而ReentrantLock必须由持有锁的线程解锁。
- A:此时为二进制信号量(Binary Semaphore),但信号量无“持有线程”概念,任何线程都可调用
-
Q:信号量能否实现线程等待通知机制?
- A:可以,但需谨慎。示例:
// 线程A发送信号 semaphore.release(); // 增加许可证 // 线程B等待信号 semaphore.acquire(); // 消耗许可证
但更推荐使用专门的等待/通知工具如Condition。
- A:可以,但需谨慎。示例:
-
Q:信号量的许可证是否必须由获取者释放?
- A:否。这是与锁的关键区别,允许“非所有者释放”,但通常应遵循“谁获取谁释放”的原则。
八、总结
信号量通过许可证机制提供灵活的并发控制,既能实现简单的互斥(许可证为1),也能实现复杂的资源池管理。理解其基于AQS的实现机制、公平/非公平模式的选择策略,以及与其他同步工具的差异,是掌握Java并发编程的重要一环。实际使用中需结合具体场景合理设置许可证数量,并注意异常情况下的资源释放。