Java 并发高频面试题:Semaphore 使用场景与常见误区
JUC(Java 并发工具包)是面试中的常客,而 Semaphore(信号量)又是 JUC 的重头戏。Semaphore 使用起来非常简单,但在生产环境的异步多线程场景中,使用不当极易引发故障。
1、Semaphore 使用场景
在面试中,关于 Semaphore 的常见问题通常围绕其应用场景展开。
面试官: 看你简历中写到你熟悉多线程编程,那你的多线程工具包有哪些工具? 候选人: 多线程 JDK 提供了丰富的工具,都集中在 JUC 包中,通常有线程池、Semaphore、CountDownLatch、原子类等。 面试官: 那你能说说信号量 Semaphore 通常在什么场景下使用呢? 候选人: 限流。
Semaphore 信号量最经典的使用场景是限流,用于控制并发度。例如对接第三方系统时,第三方接口往往限制并发度,超过并发度的请求会返回错误。此时可通过 Semaphore 发放一定数量的许可来控制并发度。
基本使用代码如下:
Semaphore semaphore = new Semaphore(10); // 指定许可数量
try {
semaphore.acquire(); // 申请许可
doSomeThing(); // 执行业务逻辑
} finally {
semaphore.release(); // 释放许可
}
说明:
- 创建 Semaphore 对象时指定该信号量中包含的许可数量。
- 执行业务方法之前,首先向信号量对象中申请一个许可。如果申请到,acquire() 方法立即返回;如果当前许可已全部申请,其他线程调用 acquire() 方法时会阻塞。
- 执行完成业务逻辑后一定要调用 release() 方法,释放许可。
注意:上面的代码存在漏洞,可能导致多释放许可。
当多个线程分别去获取许可,都能成功,但都在执行 doSomeThing() 方法的时候,如果有线程发生中断等异常导致没成功获取许可而触发异常,但最终进入到 finally 语句块进行释放许可,这样就会增加许可数量,导致逻辑异常。特别是在调用带超时时间的 acquire 方法时更加明显。正确的使用方式应确保只有在真正获取到许可后才执行逻辑,并在 finally 中安全释放。
2、许可不释放导致无限阻塞
在使用 Semaphore 结合 CompletableFuture 实现多线程并发时,若结合信号量控制调用第三方接口的并发度,可能存在许可不及时释放的问题。
示例场景:使用 CountDownLatch 控制超时时间。如果方法超时,semaphore 的 release() 方法可能不会执行,因为上述方法会超时退出。
许可不释放带来的后果非常严重,后续申请的时候由于一直没有许可,将无法获取许可,无法执行业务逻辑。
初步解决思路是在 latch.await() 方法结束后判断是否超时,如果超时,手动释放许可。但这样会造成许可的多次释放,最终导致许可数量增加,超过预期。
这要求 semaphore 的 release 方法会在不同条件下在不同地方被调用,但同一个请求只在其中一个地方被执行。
要解决这个问题,可以引入 JUC 中的原子类 AtomicBoolean。尽管多次调用,我们只需第一次调用时真正释放许可,其他调用则直接忽略即可。
解决方案如下:
引入一个包装类,包装 Semaphore,并结合 AtomicBoolean,保证每一个 SemaphoreReleaseOnlyOne 对象只会释放 Semaphore 一次。
public class {
Semaphore semaphore;
();
{
.semaphore = ();
}
InterruptedException {
semaphore.acquire();
}
{
(released.compareAndSet(, )) {
semaphore.release();
}
}
}


