简介: Semaphore是JUC中用于控制并发访问资源数量的工具,通过“许可证”机制限制同时访问特定资源的线程数,适用于数据库连接池、限流等场景,具备公平与非公平模式,是高效管理资源并发的安全利器。(239字)
在多线程的并行世界里,我们常常会遇到这样的需求:控制同时访问某个特定资源的线程数量。比如,只有10个数据库连接,却有100个线程需要访问;或者只有3个停车位,却有10辆车要停。这时,我们就需要一位高效的“交通协管员”来维持秩序,防止资源被过度使用导致系统崩溃。今天的主角——Semaphore,正是JUC包中这位不可或缺的“协管员”。
目录
- 引言
- 什么是Semaphore?
- 核心方法与工作原理
- 从生活场景到代码实战
- Semaphore的“公平”与“非公平”
- 与其他JUC工具的简单对比
- 总结与展望
- 互动环节
引言
在Java并发编程中,解决线程安全问题的核心是管理线程对共享资源的访问。除了我们熟知的synchronized和Lock这类互斥锁(一次只允许一个线程访问)之外,Semaphore(信号量)提供了一种更灵活的并发控制模型:允许多个线程同时访问一个资源,但需要限制总数。它是基于AQS(
AbstractQueuedSynchronizer)构建的经典工具,理解它有助于我们更好地掌握JUC的设计思想。
什么是Semaphore?
Semaphore(信号量) 是一个计数器,用于控制访问共享资源的线程数量。它的核心是一个“许可证”(permit)概念。
你可以把它想象成一个:
- 停车场门卫:停车场一共有N个车位(许可证)。来一辆车(线程),门卫就发放一个许可证,车辆入场。车位满了(许可证为0),新车就得在门口排队等待。走一辆车(线程释放),门卫收回一个许可证,并放行一辆等待的车。
- 门票售票处:一场音乐会只有固定数量的门票(许可证)。卖光了,后续的观众就得等待有人退票(释放)后才能买到。
在构造Semaphore时,你需要指定许可证的数量。
// 创建一个拥有 10 个许可证的信号量 Semaphore semaphore = new Semaphore(10);
核心方法与工作原理
Semaphore 的核心方法非常直观,主要围绕“获取”和“释放”许可证展开。
|
方法名 |
作用描述 |
是否阻塞 |
|
acquire() |
获取1个许可证。如果获取不到,则当前线程被阻塞,直到有许可证可用或被中断。 |
是 |
|
acquire(int permits) |
获取指定数量的许可证。 |
是 |
|
acquireUninterruptibly() |
获取1个许可证,但在等待过程中不响应中断。 |
是 |
|
tryAcquire() |
尝试获取1个许可证,成功返回true,失败立即返回false,不阻塞。 |
否 |
|
tryAcquire(long timeout, TimeUnit unit) |
在指定的超时时间内尝试获取许可证,超时后返回false。 |
限时阻塞 |
|
release() |
释放1个许可证,将其返还给信号量,从而可能唤醒一个等待的线程。 |
- |
|
release(int permits) |
释放指定数量的许可证。 |
- |
|
availablePermits() |
返回当前信号量中可用的许可证数量。 |
- |
工作原理(为什么是“交通协管员”?)
当一个线程调用 acquire() 方法时,它是在向信号量申请一个“通行许可”。信号量会检查自己的计数器:
- 如果计数器 > 0,意味着还有空位,线程成功获取许可证,计数器减1,线程继续执行。
- 如果计数器 == 0,意味着没有空位,线程会被放入一个FIFO队列中挂起等待(排队),直到有其他线程调用 release() 释放许可证(计数器加1)后,再唤醒队列中的线程来获取。
从生活场景到代码实战
让我们用经典的“停车场”例子来写一段代码。
场景:一个只有5个车位的停车场,模拟10辆汽车试图停入的情况。
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
// 模拟5个停车位
private static final int PARKING_SPACES = 5;
// 创建信号量,许可证数=5,且设置为公平模式
private static final Semaphore PARKING_SEMAPHORE = new Semaphore(PARKING_SPACES, true);
public static void main(String[] args) {
// 模拟10辆汽车
for (int carNum = 1; carNum <= 10; carNum++) {
new Thread(new CarDriver(carNum), "Car-" + carNum).start();
// 让车辆稍微错开时间到达
try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) {}
}
}
static class CarDriver implements Runnable {
private final int carNum;
public CarDriver(int carNum) {
this.carNum = carNum;
}
@Override
public void run() {
System.out.printf("[%s] 到达停车场,寻找车位...%n", Thread.currentThread().getName());
try {
// 尝试在3秒内获取停车许可
if (PARKING_SEMAPHORE.tryAcquire(3, TimeUnit.SECONDS)) {
// 成功获取许可证,停车成功
System.out.printf("[%s] ✅ 车辆%d停入车位,剩余车位:%d%n",
Thread.currentThread().getName(),
carNum,
PARKING_SEMAPHORE.availablePermits());
// 模拟车辆在车位停放一段随机时间
TimeUnit.SECONDS.sleep((long) (Math.random() * 10 + 1));
// 车辆开走,释放许可证
System.out.printf("[%s] 车辆%d驶离车位!%n", Thread.currentThread().getName(), carNum);
} else {
// 超时未获取到许可证,放弃等待,开走了
System.out.printf("[%s] ❌ 车辆%d等了3秒没车位,直接开走了!%n", Thread.currentThread().getName(), carNum);
return;
}
} catch (InterruptedException e) {
System.out.printf("[%s] 被中断了%n", Thread.currentThread().getName());
Thread.currentThread().interrupt();
} finally {
// 非常重要:无论以何种方式离开,最终都要释放许可证!
PARKING_SEMAPHORE.release();
}
}
}
}
代码解读与关键点:
- tryAcquire 与超时:我们使用了带超时参数的 tryAcquire,这在实际应用中非常有用。它避免了线程无限期阻塞,给了我们做降级处理(比如提示用户“等待超时”)的机会。
- finally 块中的 release:这是使用 Semaphore 的最佳实践和铁律!必须将 release() 操作放在 finally 块中,确保无论线程是正常执行完毕还是异常退出,都能释放占用的许可证,防止“许可证泄露”,否则会导致等待的线程永远等不到许可。
- 输出结果:运行程序,你会看到类似“Car-1停入,剩余4” -> “Car-6停入,剩余0” -> “Car-7等了3秒没车位,开走了” -> “Car-1驶离,Car-8停入”这样的日志,完美模拟了停车场饱和与调度的过程。
Semaphore的“公平”与“非公平”
在创建 Semaphore 时,第二个构造参数用于指定是否使用公平模式(Fairness)。
- new Semaphore(5, **true**):公平模式。线程获取许可证的顺序严格遵循FIFO(先来先服务)的原则,即等待时间最长的线程会优先获得许可证。这避免了线程饥饿,但性能开销稍大。
- new Semaphore(5, **false**):非公平模式(默认)。允许“插队”。当一个线程释放许可证时,信号量会直接尝试将许可证分配给任何一个正在等待的线程,而不是严格按顺序。这可能会提高整体的吞吐量,但可能导致某些线程等待时间过长(饥饿)。
选择哪种模式取决于你的业务场景对公平性和性能的权衡。
与其他JUC工具的简单对比
为了更好地理解 Semaphore,我们简单对比一下其他同步工具:
|
工具 |
核心目的 |
与Semaphore的异同点 |
|
CountDownLatch |
等待一组事件发生后再继续执行(一次性) |
不同。CountDownLatch 是“减法计数器”,不能重置,用于等待;Semaphore 是“可增减的许可证池”,可循环使用,用于控制访问。 |
|
CyclicBarrier |
让一组线程相互等待,到达一个公共屏障点后再继续执行(可循环) |
不同。CyclicBarrier 是“人齐了才开会”,所有线程是对等关系,相互等待;Semaphore 是“有票才能进”,线程与信号量交互,线程间不直接交互。 |
总结与展望
Semaphore 是JUC包中一个强大而灵活的并发控制工具。它通过“许可证”机制,优雅地解决了限制资源并发访问数的经典问题。掌握它的核心要点:
- 初始化:确定资源数量(许可证数)。
- 访问前:调用 acquire() 或 tryAcquire() 获取许可。
- 访问后:务必在 finally 块中调用 release() 释放许可。
- 模式选择:根据场景权衡公平与非公平模式。
展望未来,Semaphore 的思想不仅在Java中,在各种分布式系统、限流组件(如Sentinel、Hystrix)中也随处可见。理解了这个单机版的“协管员”,将为你在更复杂的分布式资源管理领域打下坚实的基础。
深入解析Semaphore并发控制

被折叠的 条评论
为什么被折叠?



