Java中的信号量(Semaphore)详解
字数 1907 2025-12-07 10:26:48

Java中的信号量(Semaphore)详解

一、知识点描述
信号量(Semaphore)是Java并发包(JUC)中用于控制并发访问资源数量的同步工具。它维护一组“许可证”(permits),线程访问资源前需获取许可证,访问后释放许可证,以此限制同时访问资源的线程数。信号量常用于流量控制、资源池管理、限流等场景,是解决多线程协同工作的重要工具之一。

二、核心原理与类结构

  1. 基本概念

    • 信号量内部维护一个整数计数器,表示可用许可证数量。
    • 线程通过acquire()获取许可证(计数器减1),若计数器为0则线程阻塞。
    • 线程通过release()释放许可证(计数器加1),唤醒等待线程。
  2. 类定义

    • java.util.concurrent.Semaphore
    • 主要构造方法:
      Semaphore(int permits)           // 指定许可证数量,非公平模式
      Semaphore(int permits, boolean fair) // 指定是否公平模式
      
    • 公平模式:按线程阻塞顺序分配许可证;非公平模式允许插队,性能更高。

三、核心方法详解

  1. 获取许可证

    • acquire():获取1个许可证,阻塞直到获取成功或被中断。
    • acquire(int permits):获取指定数量许可证。
    • tryAcquire():尝试获取,成功返回true,失败立即返回false。
    • tryAcquire(long timeout, TimeUnit unit):带超时的尝试获取。
  2. 释放许可证

    • release():释放1个许可证。
    • release(int permits):释放指定数量许可证。
  3. 辅助方法

    • availablePermits():返回当前可用许可证数。
    • drainPermits():获取并返回所有立即可用许可证。
    • isFair():判断是否为公平模式。

四、使用示例与场景分析

  1. 场景模拟:数据库连接池限流

    public class ConnectionPool {
        private final Semaphore semaphore;
        private final List<Connection> connections = new ArrayList<>();
    
        public ConnectionPool(int poolSize) {
            // 初始化许可证数量等于连接池大小
            semaphore = new Semaphore(poolSize);
            for (int i = 0; i < poolSize; i++) {
                connections.add(createConnection());
            }
        }
    
        public Connection getConnection() throws InterruptedException {
            semaphore.acquire();  // 获取许可证,若无可用则阻塞
            return getAvailableConnection();
        }
    
        public void releaseConnection(Connection conn) {
            returnConnection(conn);
            semaphore.release();  // 释放许可证
        }
    }
    
  2. 场景扩展:限流器(每秒钟最多10个请求)

    public class RateLimiter {
        private final Semaphore semaphore = new Semaphore(10);
        private final ScheduledExecutorService scheduler = 
            Executors.newScheduledThreadPool(1);
    
        public RateLimiter() {
            // 每秒释放所有许可证
            scheduler.scheduleAtFixedRate(() -> {
                int available = semaphore.availablePermits();
                if (available < 10) {
                    semaphore.release(10 - available);
                }
            }, 0, 1, TimeUnit.SECONDS);
        }
    
        public boolean tryRequest() {
            return semaphore.tryAcquire();  // 非阻塞尝试
        }
    }
    

五、源码关键机制解析

  1. 内部实现

    • 基于AQS(AbstractQueuedSynchronizer)的共享模式实现。
    • 许可证数量对应AQS的state变量,获取时state减少,释放时state增加。
  2. 公平与非公平差异

    // 公平模式:检查是否有前驱节点在等待
    protected int tryAcquireShared(int acquires) {
        if (hasQueuedPredecessors())  // 关键判断
            return -1;
        // ... 修改state
    }
    // 非公平模式:直接尝试修改state
    
  3. 释放操作的连锁反应

    • release()会触发AQS的doReleaseShared(),唤醒等待队列中的后继节点。
    • 被唤醒的线程会重新尝试获取许可证,形成“连锁唤醒”效果。

六、注意事项与最佳实践

  1. 许可证数量设置

    • 初始值应基于系统负载和资源容量评估。
    • 动态调整示例:
      semaphore.drainPermits();          // 清空当前许可证
      semaphore.release(newPermits);     // 设置新数量
      
  2. 避免死锁

    • 获取和释放必须成对出现,建议使用try-finally:
      semaphore.acquire();
      try {
          // 访问共享资源
      } finally {
          semaphore.release();
      }
      
  3. 与CountDownLatch/CyclicBarrier的区别

    工具 特点 适用场景
    Semaphore 控制并发数,许可证可重复使用 流量控制、资源池
    CountDownLatch 一次性屏障,等待事件完成 启动准备、结束等待
    CyclicBarrier 可重置屏障,线程相互等待 分阶段任务协同
  4. 性能优化建议

    • 非竞争场景下优先用非公平模式(默认)。
    • 高竞争场景用公平模式避免线程饥饿。
    • 使用tryAcquire()设置合理超时,避免永久阻塞。

七、常见面试问题扩展

  1. Q:信号量初始化为1时,与ReentrantLock有何区别?

    • A:此时为二进制信号量(Binary Semaphore),但信号量无“持有线程”概念,任何线程都可调用release(),而ReentrantLock必须由持有锁的线程解锁。
  2. Q:信号量能否实现线程等待通知机制?

    • A:可以,但需谨慎。示例:
      // 线程A发送信号
      semaphore.release();  // 增加许可证
      
      // 线程B等待信号
      semaphore.acquire();  // 消耗许可证
      

    但更推荐使用专门的等待/通知工具如Condition。

  3. Q:信号量的许可证是否必须由获取者释放?

    • A:否。这是与锁的关键区别,允许“非所有者释放”,但通常应遵循“谁获取谁释放”的原则。

八、总结
信号量通过许可证机制提供灵活的并发控制,既能实现简单的互斥(许可证为1),也能实现复杂的资源池管理。理解其基于AQS的实现机制、公平/非公平模式的选择策略,以及与其他同步工具的差异,是掌握Java并发编程的重要一环。实际使用中需结合具体场景合理设置许可证数量,并注意异常情况下的资源释放。

Java中的信号量(Semaphore)详解 一、知识点描述 信号量(Semaphore)是Java并发包(JUC)中用于控制并发访问资源数量的同步工具。它维护一组“许可证”(permits),线程访问资源前需获取许可证,访问后释放许可证,以此限制同时访问资源的线程数。信号量常用于流量控制、资源池管理、限流等场景,是解决多线程协同工作的重要工具之一。 二、核心原理与类结构 基本概念 : 信号量内部维护一个整数计数器,表示可用许可证数量。 线程通过 acquire() 获取许可证(计数器减1),若计数器为0则线程阻塞。 线程通过 release() 释放许可证(计数器加1),唤醒等待线程。 类定义 : java.util.concurrent.Semaphore 主要构造方法: 公平模式:按线程阻塞顺序分配许可证;非公平模式允许插队,性能更高。 三、核心方法详解 获取许可证 : acquire() :获取1个许可证,阻塞直到获取成功或被中断。 acquire(int permits) :获取指定数量许可证。 tryAcquire() :尝试获取,成功返回true,失败立即返回false。 tryAcquire(long timeout, TimeUnit unit) :带超时的尝试获取。 释放许可证 : release() :释放1个许可证。 release(int permits) :释放指定数量许可证。 辅助方法 : availablePermits() :返回当前可用许可证数。 drainPermits() :获取并返回所有立即可用许可证。 isFair() :判断是否为公平模式。 四、使用示例与场景分析 场景模拟:数据库连接池限流 场景扩展:限流器(每秒钟最多10个请求) 五、源码关键机制解析 内部实现 : 基于AQS(AbstractQueuedSynchronizer)的共享模式实现。 许可证数量对应AQS的 state 变量,获取时 state 减少,释放时 state 增加。 公平与非公平差异 : 释放操作的连锁反应 : release() 会触发AQS的 doReleaseShared() ,唤醒等待队列中的后继节点。 被唤醒的线程会重新尝试获取许可证,形成“连锁唤醒”效果。 六、注意事项与最佳实践 许可证数量设置 : 初始值应基于系统负载和资源容量评估。 动态调整示例: 避免死锁 : 获取和释放必须成对出现,建议使用try-finally: 与CountDownLatch/CyclicBarrier的区别 : | 工具 | 特点 | 适用场景 | |----------------|----------------------------------|----------------------| | Semaphore | 控制并发数,许可证可重复使用 | 流量控制、资源池 | | CountDownLatch | 一次性屏障,等待事件完成 | 启动准备、结束等待 | | CyclicBarrier | 可重置屏障,线程相互等待 | 分阶段任务协同 | 性能优化建议 : 非竞争场景下优先用非公平模式(默认)。 高竞争场景用公平模式避免线程饥饿。 使用 tryAcquire() 设置合理超时,避免永久阻塞。 七、常见面试问题扩展 Q:信号量初始化为1时,与ReentrantLock有何区别? A:此时为二进制信号量(Binary Semaphore),但信号量无“持有线程”概念,任何线程都可调用 release() ,而ReentrantLock必须由持有锁的线程解锁。 Q:信号量能否实现线程等待通知机制? A:可以,但需谨慎。示例: 但更推荐使用专门的等待/通知工具如Condition。 Q:信号量的许可证是否必须由获取者释放? A:否。这是与锁的关键区别,允许“非所有者释放”,但通常应遵循“谁获取谁释放”的原则。 八、总结 信号量通过许可证机制提供灵活的并发控制,既能实现简单的互斥(许可证为1),也能实现复杂的资源池管理。理解其基于AQS的实现机制、公平/非公平模式的选择策略,以及与其他同步工具的差异,是掌握Java并发编程的重要一环。实际使用中需结合具体场景合理设置许可证数量,并注意异常情况下的资源释放。