托卡世界我的小镇最新版本
292.54M · 2025-12-13
原创不易,禁止转载!
最近排查了一个项目中的历史遗留问题,内容涉及线程池配置调优、监控、异步并发模式。对于某些低优先级或者不重要的任务,其对应的线程池常常分配少量资源,有时甚至可有可无,线程池的拒绝策略也常常设置为抛弃策略,根据不同的需求,分别为抛弃当前和抛弃最老的任务。
然而,这种配置可能存在使得程序卡死的“bug”,且看下文。
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();
}
}
代码分析:
使用orTimeout后,运行结果如下:
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 的耗时依然是最长的那一段,由此形成级联放大效应,使得问题更加严重。