最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
Java Semaphore 实战指南:用许可证控制并发、限流和资源池
时间:2026-08-03 10:20:49 编辑:袖梨 来源:一聚教程网
Java Semaphore 实战指南:用许可证控制并发、限流和资源池的重点在于把前置条件、操作顺序和容易误判的地方分清楚。
简介
Semaphore 是 Java 并发包 java.util.concurrent 里的同步工具。

它的中文名一般叫信号量。
最容易理解的说法是:
复制代码Semaphore 像一组许可证。线程先拿许可证,拿到后才能继续执行。执行完再归还许可证。许可证用完后,后面的线程等待或直接失败。比如:
复制代码Semaphore semaphore=newSemaphore(3);表示一共有 3 个许可证。
同一时刻最多 3 个线程能拿到许可证并进入受保护的代码段。
一句话概括:
复制代码Semaphore 适合控制并发数量,比如接口并发限制、资源池、连接数控制、批量任务限流。Semaphore 解决什么问题
一个接口可能会访问下游资源:
复制代码HTTP 请求 | v订单接口 | v数据库 / Redis / 第三方接口如果瞬间来了很多请求,全部直接打到下游,可能出现:
复制代码数据库连接耗尽第三方接口超时线程池队列堆积CPU 上下文切换变多服务响应变慢Semaphore 可以在入口处加一道并发闸门:
复制代码1000 个请求 | vSemaphore(50) | +--> 同一时刻最多 50 个请求进入业务逻辑 +--> 其他请求等待、超时或直接返回繁忙它控制的是“同时正在执行的数量”。
这点和按时间窗口统计 QPS 的限流器不一样。
核心模型:许可证
Semaphore 内部维护一个许可数量。
复制代码Semaphore semaphore=newSemaphore(2);初始状态:
复制代码permits = 2线程 A 调用:
复制代码semaphore.acquire();拿走 1 个许可:
复制代码permits = 1线程 B 再拿走 1 个:
复制代码permits = 0线程 C 再调用 acquire():
复制代码没有可用许可,线程 C 阻塞等待当线程 A 执行完:
复制代码semaphore.release();归还 1 个许可:
复制代码permits = 1等待中的线程就有机会继续执行。
常用 API
| 方法 | 说明 |
|---|---|
acquire() | 获取 1 个许可,没有许可就阻塞,可响应中断 |
acquire(int permits) | 一次获取多个许可 |
acquireUninterruptibly() | 获取 1 个许可,阻塞时不响应中断 |
tryAcquire() | 尝试获取 1 个许可,立即返回成功或失败 |
tryAcquire(timeout, unit) | 在指定时间内尝试获取许可 |
release() | 释放 1 个许可 |
release(int permits) | 释放多个许可 |
availablePermits() | 查看当前可用许可数量 |
getQueueLength() | 估算正在等待许可的线程数量 |
hasQueuedThreads() | 判断是否有线程在等待 |
drainPermits() | 一次性拿走当前所有可用许可 |
最常用的是:
复制代码acquire()tryAcquire()tryAcquire(timeout, unit)release()第一个 Demo:最多 3 个线程同时执行
下面模拟 10 个任务,但同一时刻最多只允许 3 个任务执行。
复制代码package com.example.semaphore;import java.util.concurrent.Semaphore;import java.util.concurrent.TimeUnit;public classSemaphoreBasicDemo {private staticfinalSemaphoreSEMAPHORE=newSemaphore(3);public staticvoidmain(String[] args) {for (inti=1; i <= 10; i++) {int taskId= i;Thread thread=newThread(() -> runTask(taskId), "task-" + taskId); thread.start(); } } private staticvoidrunTask(int taskId) {boolean acquired=false;try { SEMAPHORE.acquire(); acquired = true; System.out.printf( "任务 %d 开始执行,可用许可=%d%n", taskId, SEMAPHORE.availablePermits() ); TimeUnit.SECONDS.sleep(2); System.out.printf("任务 %d 执行完成%n", taskId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.out.printf("任务 %d 被中断%n", taskId); } finally {if (acquired) { SEMAPHORE.release(); } } }}这里用了一个 acquired 标记。
原因很关键:
复制代码只有真正拿到许可后,finally 里才释放许可。如果线程在 acquire() 阻塞等待时被中断,实际并没有拿到许可。此时如果无条件 release(),许可数量会被凭空增加,并发控制会失效。
acquire 和 release 要成对
正确写法:
复制代码boolean acquired=false;try { semaphore.acquire(); acquired = true;// 业务逻辑} finally {if (acquired) { semaphore.release(); }}如果 acquire() 已经成功,并且业务逻辑抛异常,finally 也能释放许可。
如果 acquire() 没成功,finally 不会释放许可。
这点比直接写下面这种更稳:
复制代码try { semaphore.acquire(); // 业务逻辑} finally { semaphore.release();}后者在 acquire() 被中断时可能多释放一次。
tryAcquire:拿不到就直接拒绝
接口限流里,经常不希望请求一直排队。
如果许可已经用完,可以直接返回繁忙。
复制代码package com.example.semaphore;import java.util.concurrent.Semaphore;public classFastRejectLimiter {private final Semaphore semaphore;public FastRejectLimiter(int maxConcurrent) {this.semaphore = newSemaphore(maxConcurrent); } public String handle(int requestId) {boolean acquired= semaphore.tryAcquire();if (!acquired) {return "请求 " + requestId + " 被限流"; } try {return doBusiness(requestId); } finally { semaphore.release(); } } private String doBusiness(int requestId) {return "请求 " + requestId + " 处理成功"; }}tryAcquire() 不会阻塞。
适合:
复制代码没有容量就快速失败避免请求长期排队保护后端资源tryAcquire 超时等待
有些场景可以等一小会儿。
比如最多等 200 毫秒,拿到许可就处理,拿不到就返回繁忙。
复制代码package com.example.semaphore;import java.util.concurrent.Semaphore;import java.util.concurrent.TimeUnit;public classTimeoutLimiter {private finalSemaphoresemaphore=newSemaphore(5);public String handle(int requestId) {boolean acquired=false;try { acquired = semaphore.tryAcquire(200, TimeUnit.MILLISECONDS);if (!acquired) {return "系统繁忙,请稍后再试"; } return doBusiness(requestId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return "请求被中断"; } finally {if (acquired) { semaphore.release(); } } } private String doBusiness(int requestId) {return "request-" + requestId; }}相比 acquire() 无限等待,带超时的 tryAcquire 更适合 Web 接口。
Spring Boot 接口并发限制
下面用 Semaphore 给某个接口做并发限制。
复制代码package com.example.semaphore.web;import java.util.concurrent.Semaphore;import java.util.concurrent.TimeUnit;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController@RequestMapping("/api/reports")public classReportController {private finalSemaphorereportSemaphore=newSemaphore(10);@GetMapping("/export")public ResponseEntity<String> exportReport() {boolean acquired=false;try { acquired = reportSemaphore.tryAcquire(1, TimeUnit.SECONDS);if (!acquired) {return ResponseEntity .status(HttpStatus.TOO_MANY_REQUESTS) .body("报表导出请求过多,请稍后重试"); } String fileUrl= exportLargeReport();return ResponseEntity.ok(fileUrl); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("报表导出被中断"); } finally {if (acquired) { reportSemaphore.release(); } } } private String exportLargeReport() {return "https://cdn.example.com/report/report-10001.xlsx"; }}这个接口同一时刻最多允许 10 个导出任务执行。
其他请求最多等 1 秒,仍然拿不到许可就返回 429 Too Many Requests。
这种做法适合保护 CPU、数据库、文件导出、第三方接口等有限资源。
连接池 Demo
Semaphore 很适合控制有限资源。
下面写一个简化版连接池:
复制代码package com.example.semaphore.pool;import java.util.ArrayDeque;import java.util.Queue;import java.util.concurrent.Semaphore;import java.util.concurrent.TimeUnit;public classSimpleConnectionPool {private final Semaphore semaphore;private final Queue<MockConnection> idleConnections = newArrayDeque<>();public SimpleConnectionPool(int size) {this.semaphore = newSemaphore(size, true);for (inti=1; i <= size; i++) { idleConnections.add(new MockConnection("conn-" + i)); } } public MockConnection borrow(long timeout, TimeUnit unit)throws InterruptedException {boolean acquired= semaphore.tryAcquire(timeout, unit);if (!acquired) {return null; } synchronized (idleConnections) {MockConnection connection= idleConnections.poll();if (connection == null) { semaphore.release(); throw newIllegalStateException("许可和连接池状态不一致"); } return connection; } } public voidrelease(MockConnection connection) {if (connection == null) {return; } synchronized (idleConnections) { idleConnections.offer(connection); } semaphore.release(); } public staticclassMockConnection {private final String name;public MockConnection(String name) {this.name = name; } public voidexecute(String sql) { System.out.println(name + " 执行 SQL:" + sql); } }}测试:
复制代码package com.example.semaphore.pool;import java.util.concurrent.TimeUnit;public classConnectionPoolDemo {public staticvoidmain(String[] args) {SimpleConnectionPool pool=newSimpleConnectionPool(3);for (inti=1; i <= 8; i++) {int taskId= i;new Thread(() -> useConnection(pool, taskId), "worker-" + taskId).start(); } } private staticvoiduseConnection(SimpleConnectionPool pool, int taskId) { SimpleConnectionPool.MockConnection connection=null;try { connection = pool.borrow(1, TimeUnit.SECONDS);if (connection == null) { System.out.println("任务 " + taskId + " 获取连接超时");return; } connection.execute("select * from user where id = " + taskId); TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { pool.release(connection); } }}这个例子里:
复制代码Semaphore 控制最多同时借出几个连接Queue 保存真正的连接对象synchronized 保护 Queue 的线程安全Semaphore 只负责控制数量,不负责保存资源对象。
资源对象需要另外的数据结构管理。
公平模式和非公平模式
Semaphore 构造方法有一个 fair 参数。
非公平模式:
复制代码Semaphore semaphore=newSemaphore(5);公平模式:
复制代码Semaphore semaphore=newSemaphore(5, true);区别:
| 模式 | 说明 | 特点 |
|---|---|---|
| 非公平 | 释放许可后,后来的线程也可能先抢到 | 吞吐通常更高 |
| 公平 | 等待较久的线程优先获取许可 | 排队更有秩序,但开销更高 |
接口限流、高吞吐场景一般用默认非公平模式。
资源池、等待时间比较敏感的场景可以考虑公平模式。
一次获取多个许可
有些任务占用的资源不是 1 份。
比如批量导出任务按数据量占用不同许可。
复制代码package com.example.semaphore;import java.util.concurrent.Semaphore;import java.util.concurrent.TimeUnit;public classMultiPermitDemo {private staticfinalSemaphoreSEMAPHORE=newSemaphore(10);public staticvoidmain(String[] args) { runJob("small-job", 2); runJob("big-job", 7); runJob("medium-job", 4); } private staticvoidrunJob(String jobName, intpermits) {Thread thread=newThread(() -> {boolean acquired=false;try { SEMAPHORE.acquire(permits); acquired = true; System.out.printf("%s 获取 %d 个许可%n", jobName, permits); TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally {if (acquired) { SEMAPHORE.release(permits); System.out.printf("%s 释放 %d 个许可%n", jobName, permits); } } }); thread.start(); }}使用多个许可时,获取和释放数量要一致。
复制代码acquire(permits)release(permits)drainPermits:临时抽干许可
drainPermits() 会拿走当前所有可用许可,并返回拿走的数量。
复制代码int drained= semaphore.drainPermits();它适合临时暂停新任务进入。
复制代码package com.example.semaphore;import java.util.concurrent.Semaphore;public classDrainPermitsDemo {private finalSemaphoresemaphore=newSemaphore(5);public intpauseNewRequests() {return semaphore.drainPermits(); } public voidresumeNewRequests(intpermits) { semaphore.release(permits); }}注意,drainPermits() 只会拿走当前还没被线程占用的许可。
已经拿到许可并正在执行的线程不会被强制停止。
Semaphore 和虚拟线程
虚拟线程可以创建很多并发任务,但下游资源并不会因为虚拟线程变多而自动变强。
比如同时发起 10000 个 HTTP 调用,如果第三方接口只能承受 100 个并发,仍然需要限制并发。
Semaphore 可以和虚拟线程一起使用。
复制代码package com.example.semaphore.virtualthread;import java.util.concurrent.Executors;import java.util.concurrent.Semaphore;import java.util.concurrent.TimeUnit;public classVirtualThreadSemaphoreDemo {private staticfinalSemaphoreSEMAPHORE=newSemaphore(100);public staticvoidmain(String[] args) {try (varexecutor= Executors.newVirtualThreadPerTaskExecutor()) {for (inti=1; i <= 1000; i++) {int taskId= i; executor.submit(() -> callRemoteService(taskId)); } } } private staticvoidcallRemoteService(int taskId) {boolean acquired=false;try { acquired = SEMAPHORE.tryAcquire(2, TimeUnit.SECONDS);if (!acquired) { System.out.println("任务 " + taskId + " 获取许可超时");return; } TimeUnit.MILLISECONDS.sleep(200); System.out.println("任务 " + taskId + " 调用完成"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally {if (acquired) { SEMAPHORE.release(); } } }}虚拟线程解决的是线程成本问题。
Semaphore 解决的是下游资源并发上限问题。
两者并不冲突。
和线程池的区别
线程池也能限制并发。
比如:
复制代码Executors.newFixedThreadPool(10);它限制的是同时执行任务的线程数量。
Semaphore 限制的是进入某段代码或访问某个资源的并发数量。
对比:
| 对比项 | 线程池 | Semaphore |
|---|---|---|
| 控制对象 | 任务执行线程数 | 某段代码或资源的并发数 |
| 是否负责执行任务 | 负责 | 不负责 |
| 是否保存任务队列 | 通常有 | 没有 |
| 是否能包住局部逻辑 | 不方便 | 很方便 |
| 常见场景 | 执行异步任务 | 限制资源访问 |
两者可以一起使用:
复制代码线程池负责执行任务Semaphore 负责限制访问数据库、接口、文件等资源和其他同步工具对比
| 工具 | 作用 | 特点 |
|---|---|---|
synchronized | 互斥访问 | 同一时刻只允许一个线程进入 |
ReentrantLock | 互斥锁 | 比 synchronized 更灵活 |
Semaphore | 控制并发数量 | 同一时刻允许 N 个线程进入 |
CountDownLatch | 等待一组任务完成 | 一次性倒计时 |
CyclicBarrier | 多个线程互相等待 | 可以循环使用 |
如果资源只能一个线程访问,用锁。
如果资源允许 N 个线程同时访问,用 Semaphore。
常见问题
release 次数超过 acquire 会怎样
Semaphore 不会检查“最大许可数”。
下面这段代码会让许可数量变大:
复制代码Semaphore semaphore=newSemaphore(1);semaphore.release();semaphore.release();System.out.println(semaphore.availablePermits());输出可能是:
复制代码3所以 release() 要和成功的 acquire() 配对。
availablePermits 能不能做业务判断
availablePermits() 适合观察和监控,不适合做并发安全的业务判断。
比如:
复制代码if (semaphore.availablePermits() > 0) { semaphore.acquire();}这不是原子操作。
判断时有许可,不代表下一行一定还能拿到许可。
需要尝试获取时,使用:
复制代码boolean acquired= semaphore.tryAcquire();Semaphore 能不能做分布式限流
不能直接做分布式限流。
Semaphore 只在当前 JVM 进程内生效。
如果应用部署了 5 个实例,每个实例都有一个 Semaphore(100),整体并发上限可能变成:
复制代码5 * 100 = 500集群级限流通常需要 Redis、网关限流、服务治理框架或专门限流组件。
acquire 被中断后怎么处理
acquire() 会响应中断。
捕获 InterruptedException 后,建议恢复中断标记:
复制代码catch (InterruptedException e) { Thread.currentThread().interrupt();}这样上层代码还有机会感知中断状态。
Semaphore 是不是 QPS 限流器
Semaphore 控制的是同一时刻有多少任务正在执行。
QPS 限流控制的是单位时间内允许多少请求进入。
对比:
| 类型 | 控制目标 |
|---|---|
| Semaphore | 并发数 |
| 令牌桶 / 漏桶 | 单位时间流量 |
如果目标是“最多 100 个请求同时处理”,Semaphore 很合适。
如果目标是“每秒最多 1000 个请求”,更适合令牌桶、漏桶或网关限流。
实践建议
| 场景 | 建议 |
|---|---|
| 接口并发保护 | 使用 tryAcquire(timeout) |
| 快速失败 | 使用 tryAcquire() |
| 资源池 | Semaphore 控制数量,队列保存资源对象 |
| Web 接口 | 超时后返回 429 或业务繁忙 |
| 释放许可 | 只在成功获取许可后释放 |
| 中断处理 | 捕获后恢复中断状态 |
| 监控 | 观察 availablePermits() 和 getQueueLength() |
| 高吞吐 | 默认非公平模式 |
| 排队敏感 | 考虑公平模式 |
| 集群限流 | 使用分布式限流方案 |
小结
Semaphore 的核心是许可证。
acquire() 获取许可,release() 归还许可。许可数量决定同一时刻最多有多少线程可以进入受保护逻辑。
它和锁不同,锁通常只允许一个线程进入,Semaphore 可以允许 N 个线程同时进入。
日常项目里,Semaphore 最适合做单机并发控制:接口并发限制、连接池、导出任务保护、第三方接口保护、虚拟线程下游限流。
真正使用时,关键点不是 API 名字,而是几个工程细节:拿到许可再释放、超时获取、恢复中断状态、避免超额释放、区分并发限制和 QPS 限流。
相关文章
- Golang在Linux上性能如何 08-03
- Ubuntu Java内存管理优化 08-03
- Ubuntu Java Web应用开发实用技巧 08-03
- Ubuntu Java图形界面开发指南 08-03
- 哪种海洋动物以"再生重塑"的能力而闻名 神奇海洋1月10日答案 08-03
- Ubuntu Java兼容性问题探讨 08-03