Java 信号量Semaphore
2025-03-19 本文已影响0人
饱饱抓住了灵感
一、信号量核心概念
Semaphore是Java并发工具类,用于控制多个线程对共享资源的访问。它通过一个整数计数器表示可用资源数量:
- 初始值:表示可同时访问资源的线程数量。
- 核心方法:
-
acquire():获取一个许可(减少计数器,可能阻塞)。 -
release():释放一个许可(增加计数器,唤醒等待线程)。 -
tryAcquire(int timeout, TimeUnit unit):带超时的获取许可。
-
二、基本使用示例
场景1:限制并发线程数
假设需限制同时访问数据库的线程不超过5个:
import java.util.concurrent.Semaphore;
public class DatabaseConnectionPool {
private final Semaphore semaphore = new Semaphore(5); // 最大并发数5
public void executeQuery() {
try {
semaphore.acquire(); // 获取许可
System.out.println(Thread.currentThread().getName() + " 正在执行查询...");
Thread.sleep(1000); // 模拟数据库操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 释放许可
System.out.println(Thread.currentThread().getName() + " 查询完成");
}
}
public static void main(String[] args) {
DatabaseConnectionPool pool = new DatabaseConnectionPool();
for (int i = 0; i < 10; i++) {
new Thread(pool::executeQuery).start();
}
}
}
输出示例:
Thread-0 正在执行查询...
Thread-1 正在执行查询...
Thread-2 正在执行查询...
Thread-3 正在执行查询...
Thread-4 正在执行查询...
Thread-5 查询完成
Thread-5 正在执行查询...
...
-
原理:
Semaphore(5)初始化5个许可,前5个线程直接执行,后续线程阻塞直到许可释放。
场景2:生产者-消费者模型
使用信号量控制生产者和消费者的节奏:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.Semaphore;
public class ProducerConsumer {
private final Queue<Integer> queue = new LinkedList<>();
private final Semaphore produceSem = new Semaphore(1); // 生产许可
private final Semaphore consumeSem = new Semaphore(0); // 消费许可
private static final int MAX_QUEUE_SIZE = 3;
public void produce(int item) throws InterruptedException {
produceSem.acquire(); // 获取生产许可
queue.add(item);
System.out.println("生产: " + item + ",队列大小: " + queue.size());
if (queue.size() == MAX_QUEUE_SIZE) {
consumeSem.release(MAX_QUEUE_SIZE); // 释放对应数量的消费许可
} else {
consumeSem.release(1); // 每生产一个,允许消费一个
}
}
public int consume() throws InterruptedException {
consumeSem.acquire(); // 获取消费许可
int item = queue.poll();
System.out.println("消费: " + item + ",队列大小: " + queue.size());
produceSem.release(1); // 释放生产许可
return item;
}
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
// 生产者线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 5; j++) {
pc.produce(j);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
// 消费者线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
for (int j = 0; j < 5; j++) {
pc.consume();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
}
输出示例:
生产: 0,队列大小: 1
消费: 0,队列大小: 0
生产: 1,队列大小: 1
消费: 1,队列大小: 0
...
-
原理:通过
produceSem和consumeSem协调生产与消费速度,避免队列溢出。
三、方法详解
| 方法 | 描述 |
|---|---|
acquire() |
获取许可,若无可用许可则阻塞直到释放(可响应中断) |
release() |
释放一个许可,唤醒一个等待线程 |
tryAcquire(int n, TimeUnit t) |
尝试获取n个许可,超时后返回false(不阻塞) |
availablePermit() |
返回当前可用许可数 |
getQueuedPermits() |
返回等待获取许可的线程数 |
四、高级用法
1. 带超时的获取许可
boolean acquired = semaphore.tryAcquire(2, TimeUnit.SECONDS); // 尝试获取2个许可,2秒超时
if (acquired) {
// 成功获取
} else {
System.out.println("获取许可失败");
}
2. 释放多个许可
semaphore.release(3); // 一次性释放3个许可
五、常见问题与解决
-
死锁风险:
-
原因:线程未正确释放许可(如异常未捕获导致
finally缺失)。 -
解决:始终在
finally块中释放许可。
-
原因:线程未正确释放许可(如异常未捕获导致
-
资源泄漏:
- 原因:长时间未释放许可,导致后续线程无法获取。
-
解决:使用
tryAcquire()替代acquire(),设置超时时间。
-
性能瓶颈:
-
原因:频繁调用
acquire()/release()导致上下文切换开销大。 - 解决:批量处理资源(如批量释放许可)。
-
原因:频繁调用
六、适用场景
- 资源池控制:限制数据库连接、线程池大小等。
- 流水线并行处理:协调多个阶段的任务(如生产者-消费者)。
- 限流:控制API请求速率或文件下载并发数。
- 同步计数器:统计全局事件发生次数(需原子操作支持)。
七、总结
- 核心原则:
-
许可即资源:
Semaphore通过许可数量控制并发访问。 -
配对使用:
acquire()必须与release()成对出现。
-
许可即资源:
- 最佳实践:
- 使用
tryAcquire()避免无限期阻塞。 - 在
finally块中释放许可。 - 结合
ThreadPoolExecutor管理线程生命周期。
- 使用
- 替代方案:
-
ReentrantLock:更细粒度的锁控制。 -
CountDownLatch:一次性同步任务。 -
CyclicBarrier:多线程等待彼此到达屏障点。
-
通过合理使用Semaphore,可以高效协调多线程对共享资源的访问,避免竞争条件和死锁问题。