Java 并发工具 Semaphore 全面解析与实用案例

一、概述

在高并发场景下,如何有效地限制资源访问数量、实现“限流”是开发者常见的需求。Java 并发包 java.util.concurrent 提供了多种工具,其中 Semaphore(信号量)就是一种非常实用的资源访问控制器。它能够限制某一类资源被同时访问的线程(或进程)数量,广泛用于连接池、限流、停车场等场景。


二、名词解释

  • Semaphore(信号量):一种同步工具,用于控制同时访问某特定资源的操作数量。
  • 许可(Permit):Semaphore 内部维护的计数,每个线程获取资源时需要申请许可,许可用完则阻塞等待。
  • 公平性(Fairness):Semaphore 支持公平/非公平模式,公平模式下资源按照请求顺序分配,非公平则可能插队。

三、发展历史与权威资料

  • 最早的信号量概念由 Edsger Dijkstra 在 1965 年提出,作为进程同步的基础机制。
  • Java 1.5 引入了 java.util.concurrent.Semaphore,极大方便了多线程资源控制。
  • 参考资料:

四、项目背景与应用场景

现实中的资源访问常被限制,比如:

  • 数据库连接池:同时连接的线程数量有限,超过时需等待。
  • 停车场:车位有限,满员后新车需等待。
  • 限流服务:API 请求量达上限时拒绝或排队处理。

这些场景都需要一种机制来“限流”,Semaphore 就是最佳选择之一。


五、Semaphore 核心用法与重要方法

1. 构造方法

// 创建具有指定许可数的非公平 Semaphore
Semaphore(int permits);

// 创建具有指定许可数和公平设置的 Semaphore
Semaphore(int permits, boolean fair);

2. 重要方法详解

  • acquire() / acquire(int permits):获取许可,资源不足则阻塞。
  • release() / release(int permits):释放许可,唤醒等待线程。
  • availablePermits():当前可用许可数。
  • reducePermits(int reduction):减少许可数(谨慎使用)。
  • hasQueuedThreads():是否有线程在等待资源。
  • getQueueLength():大致等待线程数。
  • tryAcquire(int permits, long timeout, TimeUnit unit):超时尝试获取许可。
  • acquireUninterruptibly(int permits):不可中断地获取许可。

六、实战案例 —— 餐厅打饭窗口

背景

假设餐厅有 3 个打饭窗口,学生排队打饭。每次最多只有 3 个学生可以同时打饭。

核心代码

1. Semaphore 控制窗口数量
Semaphore windowSemaphore = new Semaphore(3); // 3 个窗口
2. 学生线程模拟
class Student implements Runnable {
    private Semaphore semaphore;
    private String name;

    public Student(Semaphore semaphore, String name) {
        this.semaphore = semaphore;
        this.name = name;
    }

    @Override
    public void run() {
        try {
            System.out.println(name + " 申请窗口许可...");
            semaphore.acquire();
            System.out.println(name + " 正在打饭...");
            Thread.sleep(3000); // 模拟打饭
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release();
            System.out.println(name + " 打饭结束,释放窗口许可");
        }
    }
}
3. 测试主程序
public class Test {
    public static void main(String[] args) {
        Semaphore windowSemaphore = new Semaphore(3);
        for (int i = 1; i <= 8; i++) {
            new Thread(new Student(windowSemaphore, "学生" + i)).start();
        }
    }
}

实际效果

同一时刻只允许 3 个学生打饭,其他学生自动排队等待。


七、原理剖析与与 synchronized 区别

synchronized 用于保证“同步”,即临界区同一时刻只有一个线程访问。而 Semaphore 是“互斥/限流”,可以设置同时允许多个线程访问某资源,但超过数量则阻塞。

  • 同步:保证顺序与一致性。
  • 互斥/限流:保证资源数量不被超用。

八、速记口诀

“信号量限流,许可来分流;资源有限制,排队等候用。”


九、结构图解

1. Flowchart:资源申请与释放流程

flowchart TD
    A(学生请求窗口) --> B{窗口许可是否足够?}
    B -- 是 --> C[获得许可,开始打饭]
    B -- 否 --> D[等待]
    C --> E[打饭结束]
    E --> F[释放窗口许可]
    D --> B

2. State Diagram:学生打饭状态变化

获得许可
打饭完成
释放许可
排队
打饭
结束

3. Sequence Diagram:多个学生与窗口的交互

学生 Semaphore acquire() 获得许可/阻塞 打饭(sleep 3s) release() 许可归还 学生 Semaphore

十、总结与参考资料

  • Semaphore 是限流的理想工具,适用于资源有数量限制的场景;
  • 与 synchronized 区别显著:Semaphore 是“限流”,synchronized 是“同步”;
  • 关键速记:“信号量限流,许可来分流;资源有限制,排队等候用。”
  • 推荐进一步阅读权威书籍和官方文档。

参考文献

  1. Java 官方文档:Semaphore

结语

只要掌握了 Semaphore 的原理和用法,无论是限流、连接池还是停车场问题,都可以优雅高效地解决。希望本篇图文并茂的技术博客能让你“知其然,更知其所以然”。

更多推荐