1. Semaphore基础概念与应用场景
信号量(Semaphore)是Java并发编程中一个经典的工具类,它最早由荷兰计算机科学家Dijkstra在1965年提出。在Java的JUC包中,Semaphore的实现完美体现了AQS框架的灵活性和扩展性。
1.1 Semaphore的核心作用
想象你正在管理一个共享单车停放点。这个停放点有50个停车位,当所有车位都被占用时,新来的用户必须等待直到有空位出现。Semaphore就是这样一个"停车位管理员",它维护着一组许可证(permits),每个线程在访问共享资源前必须先获取许可证,使用完毕后必须归还。
与ReentrantLock不同,Semaphore允许多个线程同时访问共享资源。这种特性使其特别适合以下场景:
- 资源池管理(数据库连接池、线程池)
- 流量控制(API限流)
- 生产者-消费者模式
- 并行任务控制
1.2 Semaphore的两种模式
Semaphore提供了公平和非公平两种工作模式:
java复制// 非公平模式(默认)
Semaphore nonFairSemaphore = new Semaphore(10);
// 公平模式
Semaphore fairSemaphore = new Semaphore(10, true);
公平模式遵循FIFO原则,保证等待时间最长的线程优先获取许可证;而非公平模式则允许"插队",虽然可能导致某些线程等待时间过长,但整体吞吐量更高。在实际应用中,非公平模式通常是更好的选择,除非有严格的公平性要求。
2. AQS框架与Semaphore实现原理
2.1 AQS共享模式解析
AbstractQueuedSynchronizer(AQS)是JUC包的核心基础框架,它通过一个FIFO队列和state变量实现了同步器的基本逻辑。AQS支持两种同步模式:
| 模式类型 | 特点 | 典型实现 |
|---|---|---|
| 独占模式 | 同一时刻只有一个线程能获取资源 | ReentrantLock |
| 共享模式 | 多个线程可以同时获取资源 | Semaphore, CountDownLatch |
Semaphore正是基于AQS的共享模式实现的。在构造Semaphore时指定的permits数量,实际上就是设置了AQS的state变量:
java复制// Semaphore.Sync的构造函数
Sync(int permits) {
setState(permits); // 将许可证数量存入AQS的state
}
2.2 关键数据结构
Semaphore的内部类结构清晰地展现了其实现原理:
java复制public class Semaphore implements java.io.Serializable {
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
// 共享模式的核心实现
}
// 非公平版本
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) {
// 先检查是否有排队线程
}
}
}
这种设计体现了模板方法模式:Sync类实现了AQS的共享模式逻辑,而NonfairSync和FairSync则分别实现了不同的获取策略。
3. 核心源码深度剖析
3.1 许可证获取流程
当线程调用acquire()方法时,背后的调用链是这样的:
java复制public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// AQS中的方法
public final void acquireSharedInterruptibly(int arg) {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
关键点在于tryAcquireShared方法的实现。以非公平模式为例:
java复制final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining;
}
}
这个自旋操作完成了两件事:
- 计算剩余许可证数量:remaining = state - acquires
- 如果remaining >= 0,尝试通过CAS更新state值
当remaining为负数时,表示许可证不足,当前线程需要进入等待队列。此时AQS会执行以下操作:
- 将当前线程包装为Node.SHARED节点
- 将节点加入CLH队列尾部
- 调用LockSupport.park()挂起线程
3.2 许可证释放机制
release()方法的实现同样简洁:
java复制public void release() {
sync.releaseShared(1);
}
// AQS中的方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
Semaphore的tryReleaseShared实现如下:
java复制protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (compareAndSetState(current, next))
return true;
}
}
释放操作的关键点:
- 通过CAS原子性地增加state值
- 调用doReleaseShared()唤醒等待队列中的线程
共享模式下的唤醒具有传播性:当一个线程被唤醒并成功获取资源后,如果还有剩余资源,它会继续唤醒后续的等待线程。这种设计大大提高了资源利用率。
4. 公平与非公平模式实现差异
4.1 非公平模式实现
非公平模式是Semaphore的默认模式,其特点是"先尝试获取,失败再排队":
java复制static final class NonfairSync extends Sync {
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
这种实现方式虽然可能导致线程饥饿,但减少了线程切换的开销,整体吞吐量更高。在大多数实际应用中,非公平模式的表现更好。
4.2 公平模式实现
公平模式在尝试获取资源前会先检查队列状态:
java复制static final class FairSync extends Sync {
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors()) // 关键区别点
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining))
return remaining;
}
}
}
hasQueuedPredecessors()方法检查当前线程是否是队列中的第一个等待线程。如果不是,直接返回-1表示获取失败。这种严格的FIFO策略保证了公平性,但也带来了额外的性能开销。
5. 高级用法与最佳实践
5.1 正确的资源管理模板
使用Semaphore时,必须确保在任何情况下都释放已获取的许可证:
java复制Semaphore semaphore = new Semaphore(10);
try {
semaphore.acquire();
// 访问共享资源
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// 处理中断
} finally {
semaphore.release();
}
5.2 批量获取与释放
Semaphore支持一次性获取/释放多个许可证:
java复制// 获取3个许可证
semaphore.acquire(3);
try {
// 执行需要多个许可证的操作
} finally {
// 释放3个许可证
semaphore.release(3);
}
这种批量操作可以提高吞吐量,但需要谨慎使用以避免资源耗尽。
5.3 超时控制
使用tryAcquire可以避免无限期等待:
java复制if (semaphore.tryAcquire(1, TimeUnit.SECONDS)) {
try {
// 成功获取许可证
} finally {
semaphore.release();
}
} else {
// 超时处理逻辑
}
6. 性能优化与问题排查
6.1 许可证数量调优
许可证数量的设置需要根据实际场景进行调整:
- 对于CPU密集型任务,许可证数量应与CPU核心数相当
- 对于IO密集型任务,可以设置更大的许可证数量
- 对于混合型任务,需要通过压测找到最佳值
6.2 常见问题排查
-
许可证泄漏:忘记调用release()会导致许可证逐渐减少,最终所有线程都被阻塞。可以通过监控state值来发现这类问题。
-
死锁风险:当多个Semaphore嵌套使用时,可能出现死锁。解决方法包括:
- 按固定顺序获取多个Semaphore
- 使用tryAcquire设置超时
- 避免在持有锁的情况下获取Semaphore
-
性能瓶颈:当许可证数量设置过小时,可能导致大量线程等待。可以通过监控等待队列长度来发现这类问题。
7. 实际应用案例
7.1 数据库连接池实现
下面是一个简化的连接池实现:
java复制public class SimpleConnectionPool {
private final List<Connection> pool;
private final Semaphore semaphore;
public SimpleConnectionPool(int size) throws SQLException {
pool = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
pool.add(createConnection());
}
semaphore = new Semaphore(size);
}
public Connection getConnection() throws InterruptedException {
semaphore.acquire();
synchronized (this) {
return pool.remove(pool.size() - 1);
}
}
public void releaseConnection(Connection conn) {
synchronized (this) {
pool.add(conn);
}
semaphore.release();
}
}
7.2 分布式限流方案
结合Redis可以实现分布式限流:
java复制public class DistributedRateLimiter {
private final Jedis jedis;
private final String key;
private final int permits;
private final Semaphore localSemaphore;
public DistributedRateLimiter(Jedis jedis, String key, int permits) {
this.jedis = jedis;
this.key = key;
this.permits = permits;
this.localSemaphore = new Semaphore(permits);
}
public boolean tryAcquire() {
if (!localSemaphore.tryAcquire()) {
return false;
}
try {
Long count = jedis.incr(key);
if (count == 1) {
jedis.expire(key, 1); // 设置1秒过期
}
return count <= permits;
} finally {
localSemaphore.release();
}
}
}
8. 扩展思考与进阶话题
8.1 Semaphore与其它同步工具对比
| 工具类 | 特点 | 适用场景 |
|---|---|---|
| Semaphore | 控制同时访问资源的线程数 | 资源池、限流 |
| CountDownLatch | 等待多个事件完成 | 初始化、启动屏障 |
| CyclicBarrier | 线程相互等待 | 并行计算 |
| Phaser | 更灵活的屏障 | 分阶段任务 |
8.2 自定义Semaphore实现
通过继承AQS,我们可以实现自己的Semaphore变种。例如,下面实现了一个可动态调整许可证数量的Semaphore:
java复制public class ResizableSemaphore {
private final Sync sync = new Sync(0);
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int permits) {
setState(permits);
}
protected int tryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 || compareAndSetState(available, remaining)) {
return remaining;
}
}
}
protected boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (compareAndSetState(current, next)) {
return true;
}
}
}
void reducePermits(int reduction) {
for (;;) {
int current = getState();
int next = current - reduction;
if (compareAndSetState(current, next)) {
return;
}
}
}
}
public void setPermits(int permits) {
int delta = permits - sync.getState();
if (delta > 0) {
sync.releaseShared(delta);
} else {
sync.reducePermits(-delta);
}
}
}
8.3 性能测试与调优建议
在实际使用中,我们需要注意:
- 在高并发场景下,非公平模式的吞吐量通常比公平模式高30%-50%
- 过小的许可证数量会导致大量线程等待,增加上下文切换开销
- 在Java 8及以后版本中,AQS的实现有显著优化,性能比早期版本更好
- 对于超高频场景,可以考虑使用基于CAS的自旋锁替代Semaphore