1. 信号量(Semaphore)的本质与核心价值
信号量这个概念最早由荷兰计算机科学家Edsger Dijkstra在1965年提出,作为操作系统进程同步的重要机制。在Java并发编程中,Semaphore类实现了这一经典概念,成为控制多线程资源访问的利器。
信号量的核心是一个计数器,它记录着可用资源的数量。这个简单的机制却蕴含着强大的能力:
- 当线程需要访问资源时,必须先从信号量获取许可(acquire)
- 如果计数器大于0,线程可以立即获取许可并继续执行
- 如果计数器为0,线程将被阻塞,直到其他线程释放许可
- 使用完资源后,线程必须释放许可(release)
这种机制特别适合控制对有限资源的并发访问。比如数据库连接池通常只有固定数量的连接,使用Semaphore可以确保不会有过多的线程同时获取连接,避免资源耗尽。
关键理解:信号量不直接管理资源,而是通过控制"访问许可"的数量来间接管理资源访问。这种间接性带来了极大的灵活性。
2. Java中Semaphore的核心API详解
Java的java.util.concurrent.Semaphore类提供了丰富的构造方法和方法:
2.1 构造方法
java复制// 创建具有给定许可数量的非公平信号量
Semaphore(int permits)
// 创建具有给定许可数量的信号量,并指定公平性
Semaphore(int permits, boolean fair)
公平性参数fair决定了线程获取许可的顺序:
- true:按照FIFO顺序授予许可
- false:允许"插队",可能提高吞吐量但可能导致某些线程饥饿
2.2 关键方法
java复制// 获取一个许可(阻塞直到可用)
void acquire()
// 释放一个许可
void release()
// 尝试获取许可(立即返回结果)
boolean tryAcquire()
// 获取指定数量的许可
void acquire(int permits)
// 释放指定数量的许可
void release(int permits)
2.3 高级方法
java复制// 返回当前可用许可数
int availablePermits()
// 减少许可数量(永久性)
void reducePermits(int reduction)
// 获取并返回所有立即可用的许可
int drainPermits()
3. Semaphore的典型使用场景
3.1 资源池管理
数据库连接池是最典型的应用场景:
java复制public class ConnectionPool {
private final Semaphore semaphore;
private final List<Connection> connections;
public ConnectionPool(int size) {
this.semaphore = new Semaphore(size);
this.connections = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
connections.add(createConnection());
}
}
public Connection getConnection() throws InterruptedException {
semaphore.acquire();
return getAvailableConnection();
}
public void releaseConnection(Connection conn) {
returnConnection(conn);
semaphore.release();
}
// ...其他实现细节
}
3.2 限流控制
在微服务架构中,Semaphore可以用来实现简单的限流:
java复制public class RateLimiter {
private final Semaphore semaphore;
public RateLimiter(int permits) {
this.semaphore = new Semaphore(permits);
}
public boolean tryAcquire() {
return semaphore.tryAcquire();
}
public void release() {
semaphore.release();
}
}
3.3 生产者-消费者问题
Semaphore可以优雅地解决经典的生产者-消费者问题:
java复制class BoundedBuffer {
private final Semaphore items;
private final Semaphore spaces;
private final Queue<Object> queue;
public BoundedBuffer(int capacity) {
this.items = new Semaphore(0);
this.spaces = new Semaphore(capacity);
this.queue = new LinkedList<>();
}
public void put(Object item) throws InterruptedException {
spaces.acquire();
synchronized(this) {
queue.add(item);
}
items.release();
}
public Object take() throws InterruptedException {
items.acquire();
Object item;
synchronized(this) {
item = queue.remove();
}
spaces.release();
return item;
}
}
4. Semaphore的高级用法与陷阱
4.1 动态调整许可数量
Semaphore允许运行时动态调整许可数量:
java复制Semaphore semaphore = new Semaphore(5);
// 需要增加容量时
semaphore.release(additionalPermits);
// 需要减少容量时
semaphore.reducePermits(reduction);
4.2 常见陷阱与解决方案
-
许可泄漏:忘记调用release()会导致许可永久丢失
- 解决方案:使用try-finally确保释放
java复制semaphore.acquire(); try { // 使用资源 } finally { semaphore.release(); } -
死锁风险:多个Semaphore使用不当可能导致死锁
- 解决方案:总是以相同的顺序获取多个Semaphore
-
性能问题:公平模式可能降低吞吐量
- 解决方案:非关键路径使用非公平模式
-
过度阻塞:大量线程等待可能导致系统不稳定
- 解决方案:使用tryAcquire()设置超时
java复制if (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) { throw new TimeoutException("等待资源超时"); }
5. Semaphore与其他同步工具的比较
5.1 与synchronized比较
| 特性 | Semaphore | synchronized |
|---|---|---|
| 并发度 | 可配置 | 固定为1 |
| 公平性 | 可配置 | 不可配置 |
| 可中断 | 支持 | 不支持 |
| 尝试获取 | 支持 | 不支持 |
| 多资源管理 | 适合 | 不适合 |
5.2 与ReentrantLock比较
| 特性 | Semaphore | ReentrantLock |
|---|---|---|
| 资源模型 | 多资源 | 单资源 |
| 可重入性 | 不可重入 | 可重入 |
| 条件变量 | 不支持 | 支持 |
| 使用场景 | 资源池 | 临界区保护 |
6. 性能优化与最佳实践
-
许可数量设置:
- CPU密集型任务:许可数≈CPU核心数
- IO密集型任务:许可数可以更高
- 公式:permits = Ncpu * Ucpu * (1 + W/C)
- Ncpu = CPU核心数
- Ucpu = 目标CPU利用率(0 < Ucpu <= 1)
- W/C = 等待时间与计算时间的比率
-
监控与调优:
java复制// 监控信号量状态
class MonitoredSemaphore extends Semaphore {
private final AtomicLong waitTime = new AtomicLong();
public MonitoredSemaphore(int permits) {
super(permits);
}
@Override
public void acquire() throws InterruptedException {
long start = System.nanoTime();
try {
super.acquire();
} finally {
waitTime.addAndGet(System.nanoTime() - start);
}
}
public long getTotalWaitTime() {
return waitTime.get();
}
}
- 与线程池配合使用:
java复制ExecutorService executor = Executors.newFixedThreadPool(10);
Semaphore semaphore = new Semaphore(5);
for (int i = 0; i < 20; i++) {
executor.submit(() -> {
semaphore.acquire();
try {
// 执行任务
} finally {
semaphore.release();
}
});
}
7. 真实案例:实现一个高性能对象池
让我们实现一个完整的、生产可用的对象池:
java复制public class ObjectPool<T> {
private final Semaphore semaphore;
private final BlockingQueue<T> pool;
private final Supplier<T> creator;
private final Consumer<T> resetter;
public ObjectPool(int size, Supplier<T> creator, Consumer<T> resetter) {
this.semaphore = new Semaphore(size);
this.pool = new LinkedBlockingQueue<>(size);
this.creator = creator;
this.resetter = resetter;
for (int i = 0; i < size; i++) {
pool.add(creator.get());
}
}
public T borrow() throws InterruptedException {
semaphore.acquire();
return pool.take();
}
public void release(T obj) {
resetter.accept(obj);
pool.offer(obj);
semaphore.release();
}
public T tryBorrow(long timeout, TimeUnit unit)
throws InterruptedException, TimeoutException {
if (!semaphore.tryAcquire(timeout, unit)) {
throw new TimeoutException();
}
T obj = pool.poll(timeout, unit);
if (obj == null) {
semaphore.release();
throw new TimeoutException();
}
return obj;
}
}
使用示例:
java复制ObjectPool<Connection> pool = new ObjectPool<>(
10,
() -> createDatabaseConnection(),
conn -> conn.reset()
);
// 使用
Connection conn = pool.borrow();
try {
// 使用连接
} finally {
pool.release(conn);
}
8. Semaphore的底层实现原理
Java中的Semaphore是基于AQS(AbstractQueuedSynchronizer)实现的:
-
状态表示:
- AQS的state字段表示可用许可数量
- acquire()会减少state
- release()会增加state
-
同步队列:
- 当许可不足时,线程会被加入CLH队列等待
- 公平模式下严格按照队列顺序唤醒
- 非公平模式下新来的线程可能"插队"
-
关键代码片段:
java复制// Semaphore.Sync内部类
abstract static class Sync extends AbstractQueuedSynchronizer {
Sync(int permits) {
setState(permits);
}
final int getPermits() {
return getState();
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
}
9. 常见面试问题与解答
-
Semaphore和锁有什么区别?
- 锁是二元信号量(只有1个许可)
- Semaphore可以控制多个许可
- 锁通常保护临界区,Semaphore通常管理资源池
-
为什么Semaphore不是可重入的?
- 设计目的不同:Semaphore用于资源计数,不是线程所有权
- 如果允许重入,可能导致许可计数不准确
-
如何避免Semaphore导致的死锁?
- 避免嵌套获取多个Semaphore
- 如果必须获取多个,确保总是以相同顺序获取
- 使用tryAcquire()设置超时
-
Semaphore的公平模式有什么优缺点?
- 优点:防止线程饥饿,保证公平性
- 缺点:降低吞吐量,增加上下文切换
-
Semaphore.availablePermits()返回的值可能不准确吗?
- 是的,因为获取许可和查询是分开的操作
- 只能作为近似参考,不能用于精确控制
10. 扩展应用:实现读写锁
使用Semaphore可以实现一个简单的读写锁:
java复制class ReadWriteLock {
private final Semaphore readLock = new Semaphore(Integer.MAX_VALUE);
private final Semaphore writeLock = new Semaphore(1);
private int readers = 0;
public void lockRead() throws InterruptedException {
readLock.acquire();
synchronized(this) {
if (readers++ == 0) {
writeLock.acquire();
}
}
}
public void unlockRead() {
synchronized(this) {
if (--readers == 0) {
writeLock.release();
}
}
readLock.release();
}
public void lockWrite() throws InterruptedException {
readLock.acquire(Integer.MAX_VALUE);
writeLock.acquire();
}
public void unlockWrite() {
writeLock.release();
readLock.release(Integer.MAX_VALUE);
}
}
这个实现虽然简单,但展示了Semaphore的强大灵活性。在实际项目中,建议使用Java标准库的ReentrantReadWriteLock,它经过了充分优化和测试。
