⭐ 推荐:社区规则条款 V1.0

如何用 Java Semaphore 控制稀缺资源的并发访问?

GJ
GJ 资深会员 👑黑卡会员
发布于 2026-10-25 18:50 ·6 浏览 ·0 回复
内容摘要

说明 Java Semaphore 通过许可计数限制数据库连接、第三方接口配额等稀缺资源的并发访问数,并给出最小示例、用 tryAcquire 设置超时以及用 finally 释放许可来避免许可泄漏的实践要点。

学完这篇你可以用 Java 的 Semaphore 把数据库连接、打印任务、第三方接口配额这类“一次只允许固定数量线程使用”的稀缺资源限制在一个确定的上限里,并且知道怎么设置超时、怎么避免许可泄漏。

第一步:先确认 Semaphore 管理的是什么

这一步要理解 Semaphore 的本质,搞清楚了才不会用错地方。Semaphore 内部维护一个许可(permit)计数器,acquire() 把计数减 1,计数为 0 时调用线程阻塞;release() 把计数加 1,唤醒等待的线程。它控制的是“同时有多少个线程能进入某段逻辑”,并不知道那段逻辑里用的是什么资源。

所以两类场景适合用它:

  • 资源实体数量有限:连接池只有 5 条连接、只有 2 台打印机、只有 3 个许可证。
  • 外部服务有并发上限:某个第三方 API 合同写明 QPS 上限,超过就限流。

注意:Semaphore 只控制并发数量,不保证被保护对象本身线程安全。比如你用 3 个许可保护一个 ArrayList,3 个线程同时写照样会出问题,那需要另外加锁或换 CopyOnWriteArrayList。

第二步:写一个最小可跑的例子

这一步要得到一个能直接运行的示例,亲眼看到并发数被卡住。新建一个类,构造函数里传入并发上限,用 new Semaphore(n) 创建:

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class LimitedTask {
    private final Semaphore permits;

    public LimitedTask(int maxConcurrent) {
        this.permits = new Semaphore(maxConcurrent);
    }

    public void run(String name) throws InterruptedException {
        permits.acquire();          // 拿不到就阻塞
        try {
            System.out.println(name + " 开始,剩余许可 "
                    + permits.availablePermits());
            Thread.sleep(1000);      // 模拟占用稀缺资源
        } finally {
            permits.release();       // 必须放在 finally
        }
    }
}

再用一个 ExecutorService 起 10 个线程调用它,传入上限 3,你会看到同一时刻最多只有 3 行“开始”打印出来。availablePermits() 返回当前剩余许可数,调试时很有用。

注意:release() 一定要写在 finally 里。中间抛异常没释放,许可就永久少了 1 个,跑一段时间后整个池子会卡死,这是最常见的事故。

第三步:给 acquire 加超时,别让线程无限等

上一步的 acquire() 没有超时,资源被长期占用时调用线程会一直挂着。这一步改成带超时的 tryAcquire,让拿不到许可的请求快速失败或者降级。

boolean got = permits.tryAcquire(500, TimeUnit.MILLISECONDS);
if (!got) {
    throw new IllegalStateException("获取许可超时,稍后重试");
}
try {
    // 使用资源
} finally {
    permits.release();
}

tryAcquire(long timeout, TimeUnit unit) 在超时时间内拿不到许可就返回 false,不会抛异常。还有 tryAcquire() 无参版本,立即返回布尔值,适合“拿不到就走缓存”的场景。

注意:acquire() 和 tryAcquire(timeout, unit) 都会抛 InterruptedException,捕获后要记得 Thread.currentThread().interrupt() 恢复中断标记,否则上层依赖中断的停止逻辑会失效。

第四步:需要排队公平时打开公平模式

默认是非公平模式,新来的线程可能插队抢走刚释放的许可,极端情况下早到的线程会饿死。这一步是针对有严格排队要求的场景。

把构造函数改成 new Semaphore(3, true),第二个参数 fair 为 true 即公平模式,按 FIFO 顺序发放许可。

注意:公平模式吞吐量会明显下降,因为要维护等待队列、频繁切换线程。除非业务确实要求先来先得(比如工单式资源分配),否则保持默认非公平模式。

第五步:封装成资源池,顺手避开三个坑

最后一步把上面的逻辑封装成一个通用方法,调用方只管传任务,不用碰 Semaphore:

public <T> T execute(Supplier<T> task, long timeoutMs) throws Exception {
    if (!permits.tryAcquire(timeoutMs, TimeUnit.MILLISECONDS)) {
        throw new IllegalStateException("资源繁忙,请稍后重试");
    }
    try {
        return task.get();
    } finally {
        permits.release();
    }
}

三个必须避开的坑:

  • release 次数必须和 acquire 匹配。多调一次 release() 等于凭空多出一个许可,上限形同虚设。想批量释放用 release(int permits),但参数要和 acquire(int permits) 对称。
  • 不要在持有时调用可能永久阻塞的方法。持有许可的线程卡在另一个锁上,整个池子会被拖死,必要时给下游调用也加超时。
  • 需要一次性拿走全部许可时用 drainPermits(),它把剩余许可清零并返回数量,适合“停止服务、拒绝新请求”的优雅关闭流程。

小结

  • Semaphore 用许可计数控制并发数量,适合连接池、许可证、外部接口配额这类稀缺资源。
  • acquire() 会阻塞,tryAcquire(超时) 更安全,生产代码优先用它。
  • release() 无条件放 finally,少一次许可泄漏,多一次上限失效。
  • 默认非公平模式吞吐高,需要先来先得才传 true。
  • Semaphore 只管并发数,不管被保护对象的线程安全,那是另一层的事。
本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2547.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 0

还没有回复,来抢沙发~