Java Semaphore 实战指南:用许可证控制并发、限流和资源池

作者:袖梨 2026-08-03

Java Semaphore 实战指南:用许可证控制并发、限流和资源池的重点在于把前置条件、操作顺序和容易误判的地方分清楚。

简介

Semaphore 是 Java 并发包 java.util.concurrent 里的同步工具。

Java Semaphore 实战指南:用许可证控制并发、限流和资源池

它的中文名一般叫信号量。

最容易理解的说法是:

 复制代码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 限流。

相关文章

精彩推荐