线程池拒绝策略避坑:谨慎使用抛弃策略,可能导致系统卡死

原创不易,禁止转载!

前言

最近排查了一个项目中的历史遗留问题,内容涉及线程池配置调优、监控、异步并发模式。对于某些低优先级或者不重要的任务,其对应的线程池常常分配少量资源,有时甚至可有可无,线程池的拒绝策略也常常设置为抛弃策略,根据不同的需求,分别为抛弃当前和抛弃最老的任务。

然而,这种配置可能存在使得程序卡死的"bug",且看下文。

问题复现

java 复制代码
public class DiscardDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(1, 1,
                0, TimeUnit.MILLISECONDS, new SynchronousQueue<>(), new DiscardPolicy());
        pool.submit(() -> {
            try {
                Thread.sleep(Duration.ofDays(1));
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        CompletableFuture<Void> cf = supplyAsync(() -> 1, pool)
                .thenAccept(System.out::println);
        cf
//                .orTimeout(3, TimeUnit.SECONDS)
//                .exceptionally(ex -> {
//                    System.out.println("exception: " + ex);
//                    return null;
//                })
                .join();

        pool.shutdownNow();
    }
}

代码分析:

  1. 配置线程池的最大线程数为1,保证同时最多仅有一个线程在运行。
  2. 使用同步队列保证没有任务存放在队列中(同步队列可以简单理解成队列长度为0的阻塞队列)。
  3. 第一个提交的任务保证长时间占用线程,第二个提交的任务必然触发拒绝策略,由于抛弃策略没有对外抛出异常或者记录日志,我们无法得知任务是未执行还是执行中或者被抛弃。这是一种典型的"黑洞"实现,缺点显而易见。
  4. 如果没有超时时间的控制,cf 对应的任务会一直处于未完成状态,程序卡死。
  5. 如果配置了超时时间(打开注释代码),由于超时时间由外部scheduler(CompletableqFuture维护)控制,可触发cf超时异常,程序顺利执行。进而整个程序可执行完成,第一个任务通过shutdownNow的中断信号触发 InterruptedException,从而线程执行结束。

使用orTimeout后,运行结果如下:

java 复制代码
exception: java.util.concurrent.TimeoutException
interrupted

监控与发现过程

观察到业务系统偶发请求"卡死",表现为必然触发超时。由于业务系统中大量使用线程池,观察到IO线程池/混合线程池(偏IO)配置存在以下问题:配置了阻塞队列(LinkedBlockingQueue),而非使用同步队列,且最大线程数量配置数量较少。

一般来说,处理普通流量时,同步队列中的任务会很快被处理,其在队列中的最大等待时间 = 队列大小 * 平均IO耗时 / 活跃线程数。当峰值流量过来且未发生熔断时,平均IO耗时不变,活跃线程数仅当队列满时增加,最大等待时间随之减少。总的来说,虽然IO线程池推荐使用 CachedThreadPool + limiter 实现,以上这种实现并不会导致很严重的问题。最理想的情况是IO线程池只处理IO请求,不占用CPU资源。

当前系统的最大问题在于最大线程池配置数量较少。实际上,增加一些IO专用线程对于系统的资源占用体现在内存消耗.

还观察到某些偶发长尾流量(如网络分区、拥堵、机器GC等)的IO任务耗时增加,两种原因都导致偶发触发拒绝策略,系统使用了 CompletableFuture、ListenableFuture 异步并发模式,某些节点卡死后,导致超时异常必然触发。

最常见的中断节点见于 allOf 方法,由于 CompletableFuture 原生的任务编排能力比较有限,这个方法必须等待所有任务执行完成,即使某些任务出现异常后,allOf 仍然不会立即返回结果。所以,即使有超时时间的控制,allOf 的耗时依然是最长的那一段,由此形成级联放大效应,使得问题更加严重。

实践建议与注意事项

  1. 推荐使用记日志 + 抛出异常的拒绝策略,虽然增加一些代码量,但是可以保证问题被及时发现。
  2. 拒绝策略触发说明系统存在瓶颈,必须认真分析,可能是请求量过大,可能是线程池配置不合理,绝对不能使用 CallerRunPolicy 等方法糊弄过去。这里最重要的原则是:如果系统接近承受极限时,必须采取流量控制、熔断等策略以保证系统的正常运行,避免出现不可用状态。
  3. 不仅可以对线程池进行监控,还可以监控任务的执行状态,比如是否执行、执行耗时、进入阻塞队列的等待时间等信息。
  4. 如果真的是低优先级并且可以抛弃的任务,可以不等,也就是说不使用get、join方法。
  5. 使用 CompletableFuture 时,配合线程池使用 AbortPolicy 拒绝策略,抛出的异常可以捕获到 CompletableFuture 内部。线程池提交任务返回的 Future 不能直接封装拒绝异常。
  6. 推荐使用 CFFU 库,其为 CompletableFuture 的辅助增强库,提供了更为强大的任务编排能力、不同的并发执行策略、backport支持以及更安全的实现。笔者参与了部分特性的实现。
相关推荐
xiaohaiAIgeo38 分钟前
【2026年】HG/T 20656-2024化工暖通空调设计规范:新版标准的变化与影响
java·前端·javascript·科普知识
weixin_431600441 小时前
NestJS 入门(7):生命周期钩子——构造函数和 `OnModuleInit` 差在哪?
前端·后端·学习·node.js·nest.js
小强库计算机毕业设计1 小时前
基于 Spring Boot + Vue3 的校园管理系统
java·spring boot·后端·项目实战·管理系统·技术分享·校园管理系统实战
霸道流氓气质1 小时前
Java中信号量(Semaphore):从本地到分布式
java·开发语言·分布式
怪奇云呼军1 小时前
G.711、Opus 和重采样会拖慢识别吗?闪电智能VoiceAgent 的音频入口怎么选
java·人工智能·python·算法·云计算·音视频
编程令我快乐1 小时前
maven部分构建参数讲解
java·maven
ZC跨境爬虫1 小时前
LeetCode 27. 移除元素(双指针详解 + Java Python 多解法对比)
java·python·leetcode
canonical_entropy1 小时前
关于《Mission Driver:Loop Engineering 的一种通用参考实现》的补充说明
后端·agent·ai编程
__zRainy__2 小时前
Node系列 · Node基础:Node.js 概述
后端·node.js
kisbad2 小时前
Day 036|OpenAI Agents SDK 快速开始:今天跑通第一个 Agent
java·前端·javascript