本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云
一个多月前,我写过一篇文章《https://mp.weixin.qq.com/s/6YWJ_-1c4L323wjHs6JohQ》,讲到的是 Spring 7 引入的@ConcurrencyLimit。最近呢,这个注解被一些老外程序员玩坏了,玩出了死锁。
更早之前,也有不少老外网友使用@ConcurrencyLimit + @Async + @Transactional,结果发现事务失效了。当然现在这个问题已经被修复了,后面就不细说了,我后面单独来说说这 3 个注解组合起来带来的问题。
今天,我们重点来聊聊@ConcurrencyLimit的可重入性、死锁、虚拟线程与最佳实践。
@ConcurrencyLimit
@ConcurrencyLimit 是 Spring 7 提供的“流量闸门”,防止我们的应用产生并发爆炸,尤其是在虚拟线程时代。
这个注解,被官方称为虚拟线程时代的安全阀。在 Java 21 以及之后的版本中,可轻松创建百万级虚拟线程的场景下,这个看似简单的注解背后,藏着一整套精密的并发控制哲学。
但老外开发者们很快发现,这个“闸门”使用不到,会出现死锁,尤其是递归调用这种重入性场景。下面我们就一一慢慢展开吧。
ConcurrencyThrottleInterceptor
@ConcurrencyLimit 并非全新发明,而是对 Spring 1.0 时代 ConcurrencyThrottleInterceptor 的现代化封装。而ConcurrencyThrottleInterceptor正是@ConcurrencyLimit的核心机制。
它的底层实现逻辑,精简后的源码如下所示。
// 来自 spring-context 模块的 ConcurrencyThrottleInterceptor
public class ConcurrencyThrottleInterceptor implements MethodInterceptor {
private final Object monitor = new Object();
private int concurrencyCount = 0;
private int concurrencyLimit = 1; // 默认限制为 1
protected void beforeAccess() {
if (this.concurrencyLimit > 0) {
synchronized (this.monitor) {
// 关键:自旋等待,直到获取许可
while (this.concurrencyCount >= this.concurrencyLimit) {
try {
this.monitor.wait(); // 阻塞等待
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted");
}
}
this.concurrencyCount++; // 占用一个槽位
}
}
}
protected void afterAccess() {
if (this.concurrencyLimit >= 0) {
synchronized (this.monitor) {
this.concurrencyCount--; // 释放槽位
this.monitor.notify(); // 唤醒一个等待线程
}
}
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
beforeAccess();
try {
return invocation.proceed(); // 执行原方法
} finally {
afterAccess(); // 确保无论如何都会释放
}
}
}
看起来非常简单,核心设计也非常简洁。
- 计数器 + 对象监视器(Monitor):经典的生产者-消费者模式
- 阻塞式等待:
wait()/notify()实现线程间协调 - finally 释放:确保异常场景下也能正确归还许可
相比大家实现的单进程限流方法,上面的实现是不是简单了很多。当然,这段代码是简化后的,实际代码比这个复杂一丢丢。
可重入性与递归调用
看懂上面的代码之后,你就会发现。@ConcurrencyLimit与可重入性是一点关系也没有,因为它压根与线程就没关系。
别看有 synchronized,它虽然支持可重入。但有“双检锁”把关,它是无法重入的,因此它默认是不可重入的!所以,在串行化以及递归场景下,就需要特别注意了,预防跳进坑里。
递归场景模拟
假设有下面这样一个递归场景,当然串行不串行的不重要,串行化更容易复现。
@Service
public class RiskyService {
@ConcurrencyLimit(1) // 限制为1(串行)
public void process(int depth) {
System.out.println("Depth: " + depth);
if (depth > 0) {
// 同一线程内递归调用自己
process(depth - 1); // 这里会死锁!
}
}
}
如果是串行化,那么执行结果很可能如下所示。
Thread-1: process(3) -> 获得许可,concurrencyCount=1
Thread-1: process(2) -> 尝试获取许可,但 concurrencyCount=1,进入 while 循环等待
Thread-1: wait() -> 自己把自己阻塞了!永久死锁!
如果不是串行化,那假设并发是 10,现在如果你的递归超过 10,那么对不起,你很可能是死锁的结果。
即使你的递归深度不超过 10,那么在并发场景下,稍微互相循环个几次,并发次数不够用了,那也会死锁。
因此,切记不要出现重复调用自己的情况。
为什么会死锁?
为什么会死锁,上面我其实说的已经很明白了,下面我在细列一下。
- 许可计数不识别线程身份:
concurrencyCount是全局计数器,不区分线程 - 同一线程无法重复获取:即使是你自己占用的许可,再次请求也会触发
wait() - 没有重入计数机制:不像
ReentrantLock有holdCount记录持有次数
重入性死锁解决方案
下面我们简单说说,假设你有递归,会有可重入的用法,那该怎么办?我这里简单推荐两个方案,供参考!
方案 1 重构代码,避免在 @ConcurrencyLimit 方法内递归。
@Service
public class SafeService {
@ConcurrencyLimit(1)
public void process(int depth) {
doProcess(depth); // 将逻辑抽离到内部方法
}
private void doProcess(int depth) { // 无注解,可自由递归
System.out.println("Depth: " + depth);
if (depth > 0) {
doProcess(depth - 1); // 安全
}
}
}
方案 2 使用 ThreadLocal 实现手动重入,这个就比较高级了,我不推荐大家这样做,搞不好会出事故的。
@Service
public class AdvancedService {
private final ThreadLocal<Integer> reentrantCount = ThreadLocal.withInitial(() -> 0);
@ConcurrencyLimit(1)
public void process(int depth) {
int currentCount = reentrantCount.get();
if (currentCount == 0) {
// 首次进入,正常获取许可
reentrantCount.set(1);
try {
doProcess(depth);
} finally {
reentrantCount.set(0);
}
} else {
// 重入调用,跳过许可获取
reentrantCount.set(currentCount + 1);
try {
doProcess(depth);
} finally {
reentrantCount.set(currentCount);
}
}
}
private void doProcess(int depth) { /* 递归逻辑 */ }
}
最重要的是,官方也不推荐这种做法,更好的方式是将并发控制与业务逻辑解耦。
虚拟线程与 wait
这里我简单说一下,明天或后面我写一篇文章来细说一下。
Spring 7 和虚拟线程对上面精简代码的关键优化如下。
- 虚拟线程被
wait()时,底层载体线程(Carrier Thread)会被释放 - 实现近乎零成本的阻塞等待
- 可以设置更高的并发限制(如 1000+)而不担心资源耗尽
wait 它变了,变得更好用了。在之前,wait 可是不会释放线程本身的。具体的细节,后面我单独写文章来说。
同时,这里也说一下,更高级的场景。比如@ConcurrencyLimit与 @Async、@Retryable、@Transactional 组合的协同,后面我单独来说。之前的候选预览版本就发生过@Async + @ConcurrencyLimit + @Transactional导致事务失效的 bug,这里我们暂不细表。
最佳实践
下面,我根据社区网友的讨论,总结了 Spring 7 并发控制的 10 条军规,供大家参考!
限制值设定
要坚信科学而不是猜测或随机或看个人喜好设置。
// CPU 密集型任务(计算、加密)
@ConcurrencyLimit(Runtime.getRuntime().availableProcessors())
// IO 密集型任务(HTTP、DB)
@ConcurrencyLimit(50) // 根据连接池大小调整
// 外部 API 调用(已知限流)
@ConcurrencyLimit(10) // 匹配下游服务的 QPS 限制
将并发控制提到服务编排层
避免自己调自己,避免在 @ConcurrencyLimit 方法内同步调用其他 @ConcurrencyLimit。
// 嵌套调用可能导致死锁
@ConcurrencyLimit(5)
public void methodA() {
methodB(); // 如果 methodB 也需要许可,可能死锁
}
@ConcurrencyLimit(5)
public void methodB() {
// ...
}
// 将并发控制上提到服务编排层
@ConcurrencyLimit(5)
public void orchestrate() {
internalMethodA(); // 无注解
internalMethodB(); // 无注解
}
虚拟线程增大限制
如果是虚拟线程环境,则推荐设置更大的限制。
// 传统平台线程
@ConcurrencyLimit(10)
// 虚拟线程(轻量,可设置更大)
@ConcurrencyLimit(200)
监控与可观测性
为了更好的提升服务稳定性,可以建立或自行提供监控与可观测性。
@ConcurrencyLimit(10)
public void monitoredMethod() {
Metrics.gauge("concurrency.current",
getCurrentConcurrency());
Metrics.gauge("concurrency.waiting",
getWaitingThreads());
// 业务逻辑
}
// 获取当前并发数(需要获取拦截器内部状态)
private int getCurrentConcurrency() {
// 建议自定义拦截器暴露指标
return MyConcurrencyMetrics.getActiveCount();
}
超时控制
一定要注意设置超时时间,预防超时问题,否则不管多大的并发控制,都会被耗干。
@ConcurrencyLimit(5)
public void processWithTimeout() throws InterruptedException {
// 设置最大等待时间,防止无限阻塞
boolean acquired = tryAcquirePermit(10, TimeUnit.SECONDS);
if (!acquired) {
throw new TaskRejectedException("Concurrency limit timeout");
}
try {
// 业务逻辑
} finally {
releasePermit();
}
}
需要特别注意的是@ConcurrencyLimit 本身不提供超时配置,需要配合 SimpleAsyncTaskExecutor.setTaskTerminationTimeout() 或自定义实现。
拒绝策略
推荐设置合适的拒绝策略,比如类似下面的代码。
@Bean
public SimpleAsyncTaskExecutor asyncExecutor() {
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
executor.setConcurrencyLimit(10);
// 超限直接拒绝
executor.setRejectTasksWhenLimitReached(true);
return executor;
}
类级别 vs 方法级别
@ConcurrencyLimit 也可以作用到类上,它们的区别如下。
// 类级别:所有方法共享同一个限制
@ConcurrencyLimit(20)
@Service
public class SharedResourceService {
public void method1() {} // 共享 20 个许可
public void method2() {} // 共享 20 个许可
}
// 方法级别:各自独立限制
@Service
public class IndependentService {
@ConcurrencyLimit(10)
public void method1() {} // 独立的 10 个许可
@ConcurrencyLimit(5)
public void method2() {} // 独立的 5 个许可
}
测试策略
自己写的代码,一定要严格测试一下,要不发可能会发生意想不到的事故,遇到一个 3.25 的绩效就不好了。
@SpringBootTest
class ConcurrencyLimitTest {
@Autowired
private ResilientService service;
@Test
void testConcurrencyLimitActuallyWorks()
throws InterruptedException {
CountDownLatch startLatch = new CountDownLatch(1);
CountDownLatch endLatch = new CountDownLatch(10);
AtomicInteger maxConcurrent = new AtomicInteger(0);
AtomicInteger currentConcurrent = new AtomicInteger(0);
// 同时启动 10 个任务,但限制为 3
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
startLatch.await(); // 等待同时启动信号
service.limitedMethod(currentConcurrent, maxConcurrent);
} catch (Exception e) {
// 忽略中断
} finally {
endLatch.countDown();
}
}).start();
}
startLatch.countDown(); // 同时启动
assertTrue(endLatch.await(30, TimeUnit.SECONDS));
// 验证最大并发数不超过限制
assertTrue(maxConcurrent.get() <= 3,
"Actual max: " + maxConcurrent.get());
}
}
与 Resilience4j 的对比选择
@ConcurrencyLimit 好就好在不需要其它依赖,下面是它们之间的简单对比。
| 特性 | Spring 7 @ConcurrencyLimit | Resilience4j RateLimiter |
|---|---|---|
| 并发控制 | 阻塞等待(Semaphore 模式) | 令牌桶算法 |
| 超时支持 | 需手动实现 | 内置超时 |
| 动态配置 | 需重启/刷新 Bean | 支持动态配置 |
| 监控指标 | 需自定义 | 内置 Micrometer |
| 适用场景 | 简单场景、虚拟线程 | 复杂微服务治理 |
这里我建议:单体应用或简单场景用 @ConcurrencyLimit,微服务网格用 Resilience4j。
虚拟线程专属调优
推荐和虚拟线程一起使用。
@Configuration
@EnableAsync
public class VirtualThreadConfig {
@Bean
public SimpleAsyncTaskExecutor virtualThreadExecutor() {
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
executor.setVirtualThreads(true);
executor.setConcurrencyLimit(500); // 可设置更大
executor.setThreadNamePrefix("vt-task-");
return executor;
}
}
@Service
public class VirtualThreadService {
@Async("virtualThreadExecutor")
@ConcurrencyLimit(500) // 匹配执行器限制
public CompletableFuture<Void> highConcurrencyTask() {
// 可安全启动大量虚拟线程
return CompletableFuture.completedFuture(null);
}
}
限于篇幅和时间,我就写到这里吧,下面我们简单总结一下。
总结
@ConcurrencyLimit 是 Spring 7 在虚拟线程时代的重大创新,但它不是银弹。
那么什么时候用它呢?
- 保护外部 API、DB 连接等有限资源
- 在虚拟线程环境中防止资源过载
- 简单场景的快速限流
那么什么时候不要用呢?
- 需要复杂限流算法(令牌桶、滑动窗口)
- 需要动态调整限制值
- 方法内有递归调用(会死锁!)
- 需要非阻塞响应式编程(用
Mono.flatMap+limitRate替代)
记住下面这个黄金法则。
如果方法会递归 -> 避免使用 @ConcurrencyLimit
如果使用虚拟线程 -> 可以设置更大的限制
如果组合多个注解 -> 务必充分测试调用后的行为

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!
本文原文出处:业余草: » @ConcurrencyLimit滥用引发死锁,Spring7+虚拟线程不是银弹