故障现象

为提升接口响应速度,将聚合接口的串行调用改为异步并行。上线后不久,系统抛出大量异常:

1
2
3
4
5
6
Exception in thread "main" java.util.concurrent.ExecutionException: 
java.util.concurrent.RejectedExecutionException:
Task java.util.concurrent.FutureTask@42936575
[Not completed, task = xxxxx] rejected from
java.util.concurrent.ThreadPoolExecutor@33f18ac
[Running, pool size = X, active threads = X, queued tasks = N, completed tasks = M]

线程池和队列均已被耗尽。

第一反应是线程池参数设置太小,但深入分析后发现:代码逻辑有问题——提交到线程池的任务存在相互依赖关系。

核心问题:在有限大小的线程池中执行相互依赖的任务,可能产生死锁。

原因分析:线程饥饿锁是如何产生的

复现环境

定义一个资源紧张的线程池(用于快速复现问题):

1
2
3
4
5
6
7
8
9
10
11
12
private static final ExecutorService poolExecutor = new ThreadPoolExecutor(
2, 2, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(1),
new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(),
new ThreadPoolExecutor.AbortPolicy() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
log.error("rejectedExecution");
super.rejectedExecution(r, e);
}
}
);

问题代码模式

业务逻辑:任务 A 依赖任务 B 的结果,两者被提交到同一个线程池

1
2
3
4
5
6
// 主线程提交任务A
Future<?> futureA = poolExecutor.submit(() -> {
// 任务A内部又提交任务B,并等待其结果
Future<?> futureB = poolExecutor.submit(() -> { /* 任务B */ });
return futureB.get(); // 阻塞等待B完成
});

死锁形成过程

步骤 线程1 线程2 队列
1 执行任务A(提交B,阻塞等待) 执行任务A(提交B,阻塞等待) B, B
2 占用线程,等待B完成 占用线程,等待B完成 B, B(无线程执行)
3 死锁:线程全被占用,队列任务无法执行

关键机制

  • 任务 A 占用线程池线程,等待任务 B 完成
  • 任务 B 在队列中排队,等待空闲线程
  • 当所有线程都被”等待中的 A”占满时,B 永远无法执行 → 死锁

解决方案对比

❌ 方案一:扩大线程池

增大线程池或改用无界线程池。

问题:线程资源宝贵,无法无限扩容,治标不治本。

❌ 方案二:Future.get 加超时

1
future.get(timeout, TimeUnit.SECONDS);

问题:超时后任务失败,业务功能异常,属于”掩盖问题”。

⚠️ 方案三:CallerRunsPolicy 拒绝策略

1
new ThreadPoolExecutor.CallerRunsPolicy();

问题:异步退化为串行,虽能避免故障,但会让 Web 容器线程执行业务逻辑,存在线程池污染风险。

✅ 方案四:线程池隔离

相互依赖的任务使用不同的线程池:

1
2
3
4
5
6
7
ExecutorService poolA = Executors.newFixedThreadPool(2);
ExecutorService poolB = Executors.newFixedThreadPool(2);

poolA.submit(() -> {
Future<?> futureB = poolB.submit(() -> { /* 任务B */ });
return futureB.get();
});

优点:彻底避免资源竞争。
缺点:线程池数量难规划,为少量依赖任务单独建池成本高。

✅⭐ 方案五:CompletableFuture 任务编排(推荐)

CompletableFuture 显式表达任务依赖,无需手动管理线程池:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义异步任务
CompletableFuture<String> taskA = CompletableFuture.supplyAsync(() -> {
return "A的结果";
});

// 任务B依赖A的结果
CompletableFuture<String> taskB = taskA.thenComposeAsync(resultA -> {
return CompletableFuture.supplyAsync(() -> {
return resultA + " + B的处理";
});
});

// 等待完成
String result = taskB.join();

核心优势

  • 依赖关系显式化,避免隐式阻塞
  • 框架自动优化线程使用,防止饥饿
  • 支持链式编排、组合、异常处理

总结

方案 适用场景 推荐指数
扩大线程池 临时应急
超时机制 快速失败场景
CallerRunsPolicy 降级保护 ⭐⭐
线程池隔离 依赖关系明确的稳定场景 ⭐⭐⭐
CompletableFuture 复杂异步编排 ⭐⭐⭐⭐⭐

最佳实践

  1. 避免在有限线程池中提交相互依赖的阻塞任务
  2. 优先使用 CompletableFuture 进行异步任务编排
  3. 如必须用线程池隔离,确保依赖关系清晰可维护