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

原创不易,禁止转载!

前言

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

然而,这种配置可能存在使得程序卡死的"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支持以及更安全的实现。笔者参与了部分特性的实现。
相关推荐
Slow菜鸟4 分钟前
Codex CLI 教程(五)| AI 驱动项目从零到一:面向 Java 全栈工程师打造个人 ECC(V2版)
java·开发语言·人工智能
月落归舟13 分钟前
java基础之拷贝、单例
java·单例·拷贝
鬼蛟17 分钟前
什么是 Git
java
李日灐34 分钟前
< 6 > Linux 自动化构建工具:makefile 详解 + 进度条实战小项目
linux·运维·服务器·后端·自动化·进度条·makefile
蝎子莱莱爱打怪35 分钟前
小孩儿才做选择!Hermes 和OpenClaw 我都要!
人工智能·后端·github
直奔標竿38 分钟前
SpringAI + RAG + MCP + Agent 零基础全栈实战(完结篇)| 27课完整汇总,Java开发者AI转型必看
java·开发语言·人工智能·spring boot·后端·spring
云烟成雨TD39 分钟前
Spring AI 1.x 系列【31】向量数据库:进阶使用指南
java·人工智能·spring
枫叶林FYL1 小时前
项目八 云资源成本优化与治理平台
后端·python·自然语言处理·flask
万邦科技Lafite1 小时前
京东开放API接口:item_get返回参数指南
java·前端·javascript·api·电商开放平台
曹牧1 小时前
Java:处理 HTTP 请求的 Content-Type
java·开发语言