线程池中的隐形陷阱:任务依赖导致的线程饥饿锁


故障描述

为提高系统吞吐量,优化接口的响应速度,让页面响应时间更短,将某个聚合接口的多个串行调用更改为异步并行的方式
上线后,不到一会出现大量的线程池资源耗尽的异常告警,异常日志:

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]

大量任务被拒绝,原因是线程池和队列均已被耗尽,看到该异常第一反应是线程池的最大线程数和队列设置太小,但是仔细分析发现业务代码写的有问题,线程池提交的任务存在相互依赖

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

故障代码

为复现问题,定义一个有限大小的线程池,线程池大小:2,队列大小:1,拒绝策略:AbortPolicy


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任务完成的同时,占用的线程不会结束,如果流量足够,线程池里的线程都被A任务占用完而不会结束,那么在任务队列的B任务永远不会有线程去执行,从而出现了线程饥饿锁的出现。

如何避免

  1. 设置更大的线程池大小或者选择不受限制的线程池 ❌
    虽然更大的线程池能够减少或者避免线程饥饿锁的出现,但是线程资源是宝贵的,不可能无限创建,该方法有些不合理

  2. 使用java.util.concurrent.Future#get(long, java.util.concurrent.TimeUnit)
    使用带超时时间的Future.get虽然能够让后续的任务尽快返回,不阻塞接口,但是后续请求的功能是非正常的,这种方式明显不合理

  3. 使用线程池拒绝策略为:CallerRunsPolicy ⚠️
    将拒绝策略更改为CallerRunsPolicy,因为线程池的大小以及流量无法确定,那么在线程异常时将异步执行退化为串行执行,也不失为一种避免线上故障的方法,但是在饥饿锁的场景下会让web容器的线程也受到影响

  4. 使用不同的线程池隔离有互相依赖的任务 ✅
    有相互依赖的任务,隔离到不同的线程池中执行,使得相互之间不再竞争使用相同的线程池资源,理论上的好方法,但是在实际业务中,我们经常无法区分出哪些业务应该归拢到一个线程池中,哪些应该分开创建线程池,而且线程资源宝贵,仅仅是因为相互依赖的任务就创建不同的线程池也不是一个好的方式

  5. 使用CompletableFuture + 自定义线程池来编排存在相互依赖的任务 ✅ 🌟

public static void main(String[] args)  {

    CompletableFuture<Void> futureA = CompletableFuture.supplyAsync(() -> null)
        .thenComposeAsync(result1 -> CompletableFuture.supplyAsync(() -> null));

    CompletableFuture<Void> futureB = CompletableFuture.supplyAsync(() -> null)
        .thenComposeAsync(result2 -> CompletableFuture.supplyAsync(() -> null));

    // 等待所有的任务完成
    CompletableFuture.allOf(futureA, futureB).join();
}

使用 CompletableFuture 可以更清晰地表达任务之间的依赖关系,而不需要显式地操作线程池和 Future,更方便地编排存在相互依赖的任务,避免线程饥饿问题,并提供更灵活和强大的异步编程能力。

小结

不要在有限大小的线程池中,执行有相互依赖的任务,防止线程饥饿锁导致故障。我们可以将相互依赖的任务隔离到不同的线程池中执行,或者使用CompletableFuture + 自定义线程池来编排相互依赖的任务。


文章作者: gloamfox
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 gloamfox !
 上一篇
从JUnit到Mockito:构建可靠Java单元测试
本文详细介绍了Java单元测试中的核心框架JUnit 4和JUnit 5的区别与改进,以及Mockito和PowerMock的使用方法。文章涵盖了注解、断言、测试运行器等基础概念,并通过实际示例展示了如何编写高效的单元测试,包括模拟对象、参数化测试、静态方法模拟等高级功能。
2025-11-18
下一篇 
Zookeeper分布式服务框架
Zookeeper是Apache Hadoop的子项目,主要用于解决分布式应用中的数据管理问题。本文详细介绍了Zookeeper的系统原理,包括文件系统(四种znode类型)和监听通知机制,并阐述了其在配置管理、命名服务、分布式锁和集群管理等方面的应用。此外,还探讨了Zookeeper的设计目的和集群中的三种角色:领导者、跟随者和观察者。
2025-11-18
  目录