当前位置: 首页 > 图灵资讯 > java面试题> 解释Java中的信号量(Semaphore)及其实现细节

解释Java中的信号量(Semaphore)及其实现细节

来源:图灵教育
时间:2025-02-19 10:46:31

什么是信号量(Semaphore)?

信号量是一种用于控制并发访问资源的工具。它可以看作是一个计数器,表示某个资源当前还有多少个“许可证(permit)”可以被线程使用。线程想要访问资源时,先“拿”一个许可证;用完资源后,再“归还”许可证。如果没有可用的许可证,线程就会被阻塞,直到有许可证可用。

可以把它想象成一个公共停车场:

  • 停车场有固定数量的停车位(许可证)。
  • 当车(线程)进入时,需要占用一个停车位(拿一个许可证)。
  • 如果停车位满了(许可证用完了),车就得在外面排队(线程阻塞)。
  • 当有车离开时,会释放一个停车位(归还许可证),这样后面的车才能进入。

Java 中的 Semaphore

在 Java 中,Semaphore 是 java.util.concurrent 包下的一个类,用来实现上述信号量功能。它有两种模式:

  1. 公平模式:按照线程请求许可证的顺序(FIFO)来分配许可证。
  2. 非公平模式(默认):线程可能会“抢占”许可证,优先级不一定按顺序。

核心方法

  1. acquire():尝试获取一个许可证。如果没有可用的许可证,线程会阻塞,直到有许可证可用。
  2. release():释放一个许可证,把它还回信号量。
  3. tryAcquire():尝试立即获取一个许可证,如果没有可用的许可证,不会阻塞,而是直接返回 false
  4. 构造函数
    • Semaphore(int permits):创建一个指定初始许可证数量的信号量。
    • Semaphore(int permits, boolean fair):在指定许可证数量的基础上,决定是否使用公平模式。

信号量的使用场景

  1. 限制资源访问
    如果有多个线程需要访问有限的资源(比如数据库连接池或线程池中的线程),可以用信号量来限制同时访问的线程数量。

  2. 实现流量控制
    在高并发场景中,可以用信号量限制每秒处理的请求数量,防止系统过载。

  3. 控制多线程任务的执行顺序
    通过信号量,可以让某些线程等待其他线程完成某个任务后再继续执行。


Semaphore 的实现细节

1. 许可证的管理

Semaphore 的核心是一个计数器(permits),表示当前可用的许可证数量。这个计数器的初始值由构造函数指定。

  • 每次调用 acquire() 时,计数器会减 1。
  • 每次调用 release() 时,计数器会加 1。

计数器的值可能为:

  • 正数:表示还有足够的许可证可用。
  • 零:表示许可证用完了,线程需要等待。
  • 负数:不会出现,因为线程会阻塞直到许可证可用。

2. 底层依赖 AQS

Semaphore 的底层实现依赖于 AbstractQueuedSynchronizer(AQS),这是 Java 并发包中一个非常重要的工具类,用来管理线程的同步状态和阻塞队列。

  • 状态变量:AQS 的状态变量用来表示当前的许可证数量。
  • 线程等待队列:当许可证不足时,线程会被放入 AQS 的等待队列中,直到有线程调用 release() 释放许可证。

3. 公平与非公平模式

  • 非公平模式(默认):线程可能会“抢占”许可证,即使有其他线程已经在等待队列中排队。这样可以提高整体吞吐量。
  • 公平模式:线程会严格按照请求的顺序获取许可证,先到先得。这种模式的性能可能会稍微差一些,因为需要更多的线程调度。

4. 阻塞与唤醒

  • 当线程调用 acquire() 时,如果没有可用许可证,线程会进入等待状态(阻塞)。
  • 当另一个线程调用 release() 时,信号量会唤醒等待队列中的一个线程,让它重新尝试获取许可证。

5. 可重入性

Semaphore 本身不是可重入的。也就是说,如果一个线程多次调用 acquire(),它需要多次调用 release() 才能释放所有的许可证。


示例场景:停车场模型

假设有一个停车场,最多只能容纳 3 辆车(许可证数量为 3)。多个线程模拟车辆尝试进入停车场。

  • 当停车位有空余时,车辆直接进入。
  • 如果停车位满了,车辆必须等待。
  • 当有车辆离开时,停车位变空,等待的车辆可以进入。

使用 Semaphore,可以轻松实现这种逻辑。


总结

  1. Semaphore 是什么?
    它是一种计数器,用来限制同时访问某个资源的线程数量。

  2. 如何工作?

    • 线程调用 acquire() 获取许可证,计数器减 1。如果没有可用的许可证,线程会阻塞。
    • 线程调用 release() 归还许可证,计数器加 1,唤醒等待的线程。
  3. 适用场景

    • 限制资源的并发访问(如数据库连接池)。
    • 流量控制(如限制同时处理的请求数)。
    • 控制线程执行顺序。
  4. 底层实现

    • 基于 AQS(AbstractQueuedSynchronizer)。
    • 许可证数量通过 AQS 的状态变量管理。
    • 公平与非公平模式决定许可证分配的策略。