
1. 引言线程池是 Java 并发编程中绕不开的话题也是面试高频考点。然而在实际生产环境中线程池「爆掉」的问题屡见不鲜任务堆积、拒绝异常、线程数失控、CPU 飙升……这些问题往往源于对线程池核心参数的理解不够深入或者配置时凭感觉拍脑袋。本文将从线程池的核心参数讲起逐步深入到队列与拒绝策略的选择再给出 CPU 密集与 IO 密集场景下的配置建议最后分享线程池耗尽的排查思路以及如何通过自定义 ThreadFactory 和监控手段让线程池更可控、更可观测。2. 线程池核心参数Java 的ThreadPoolExecutor是线程池的核心实现它的构造函数有七个参数其中最关键的是以下四个核心线程数corePoolSize线程池中常驻的线程数量。即使线程空闲只要不超过核心线程数线程也不会被回收。最大线程数maximumPoolSize线程池允许创建的最大线程数量。当任务队列已满且当前线程数小于最大线程数时会继续创建新线程。任务队列workQueue用于存放等待执行任务的阻塞队列。当线程数达到核心线程数后新任务会先进入队列排队。拒绝策略handler当任务队列已满且线程数已达到最大线程数时对新提交任务的处理策略。ThreadPoolExecutorexecutornewThreadPoolExecutor(corePoolSize,// 核心线程数maximumPoolSize,// 最大线程数keepAliveTime,// 空闲线程存活时间TimeUnit.SECONDS,// 时间单位workQueue,// 任务队列threadFactory,// 线程工厂handler// 拒绝策略);2.1 核心线程数与最大线程数核心线程数是线程池的「保底兵力」最大线程数是线程池的「兵力上限」。两者之间的差值决定了线程池在高峰期能临时扩充多少线程。需要特别注意的是只有当任务队列已满时线程池才会创建超过核心线程数的线程。如果队列没有满即使最大线程数设置得很大线程池也只会保持核心线程数规模的线程在工作。2.2 任务队列任务队列的选择直接影响线程池的行为。常见的队列有以下几种LinkedBlockingQueue无界队列默认容量为Integer.MAX_VALUE。使用无界队列时maximumPoolSize参数将失去意义因为任务永远不会触发队列满也就不会创建超过核心线程数的线程。ArrayBlockingQueue有界队列需要指定容量。队列满后才会触发扩容线程的逻辑。SynchronousQueue不存储任务的队列每个任务必须立即交给一个线程执行否则就阻塞或触发拒绝策略。适合任务量小但执行快的场景。2.3 拒绝策略当队列已满且线程数达到最大值时新提交的任务会触发拒绝策略。JDK 提供了四种内置策略AbortPolicy默认直接抛出RejectedExecutionException中断任务的提交。CallerRunsPolicy不抛异常而是由提交任务的线程自己执行该任务。这能起到一定的「背压」作用减缓任务提交速度。DiscardPolicy静默丢弃任务不抛异常也不执行。DiscardOldestPolicy丢弃队列中最旧的任务然后重新尝试提交当前任务。3. CPU 密集 vs IO 密集怎么配线程池的核心线程数没有「银弹」公式需要根据任务类型来估算。3.1 CPU 密集任务CPU 密集任务的特点是任务几乎不等待一直在占用 CPU 计算。此时线程数不宜过多否则线程切换反而会降低吞吐。推荐配置核心线程数 ≈ CPU 核数或核数 1。intcpuCoresRuntime.getRuntime().availableProcessors();ThreadPoolExecutorexecutornewThreadPoolExecutor(cpuCores,cpuCores,0L,TimeUnit.MILLISECONDS,newLinkedBlockingQueue(1000));3.2 IO 密集任务IO 密集任务的特点是任务大部分时间在等待 IO网络、磁盘、数据库CPU 利用率不高。此时可以配置更多的线程让等待期间其他线程继续执行。推荐配置核心线程数 ≈ CPU 核数 × 2或按公式CPU 核数 / (1 - 阻塞系数)估算阻塞系数通常取 0.8~0.9。intcpuCoresRuntime.getRuntime().availableProcessors();intioThreadscpuCores*2;ThreadPoolExecutorexecutornewThreadPoolExecutor(ioThreads,ioThreads*2,60L,TimeUnit.SECONDS,newArrayBlockingQueue(500));3.3 配置要点队列容量要结合任务峰值队列太小容易触发拒绝策略太大则可能导致任务积压、内存膨胀。最大线程数要留有余量避免高峰期线程数打满后新任务直接被拒绝。拒绝策略要按业务选对可靠性要求高的任务建议用CallerRunsPolicy或自定义策略对可丢弃的日志类任务可以用DiscardPolicy。4. 线程池耗尽排查线程池「爆了」通常表现为任务被拒绝、接口超时、线程数持续打满。排查思路如下4.1 看监控指标先确认线程池的实时状态当前活跃线程数是否长期接近maximumPoolSize。队列积压量队列中堆积的任务数量是否持续增长。拒绝次数是否频繁触发拒绝策略。4.2 定位任务来源通过日志或链路追踪找到提交任务最多的调用方。常见原因包括上游接口并发突增超出线程池承载能力。某个任务执行过慢如慢 SQL、远程调用超时导致线程被长期占用。任务内部又提交了子任务形成递归提交线程池被「套娃」耗尽。4.3 检查线程池配置核心线程数是否过小导致任务长期排队。队列是否无界导致任务积压但线程数不增长。keepAliveTime是否过短导致空闲线程被频繁回收和重建。4.4 常见修复手段调大核心线程数或最大线程数。改用有界队列并设置合理的容量。优化任务执行逻辑缩短单任务耗时。对任务做熔断或限流避免瞬时洪峰打垮线程池。5. 自定义 ThreadFactory 和监控5.1 自定义 ThreadFactory默认的线程工厂创建的线程名称是pool-N-thread-M排查问题时很难定位是哪个业务线程池。自定义ThreadFactory可以给线程起有意义的名称并设置守护线程等属性。ThreadFactorythreadFactorynewThreadFactory(){privatefinalAtomicIntegercounternewAtomicInteger(1);OverridepublicThreadnewThread(Runnabler){ThreadthreadnewThread(r,order-pool-counter.getAndIncrement());thread.setDaemon(false);returnthread;}};这样在jstack或监控平台里就能一眼看出是哪个业务线程池在占用资源。5.2 线程池监控ThreadPoolExecutor本身提供了几个关键的监控方法getPoolSize()当前线程池中的线程总数。getActiveCount()当前活跃线程数。getQueue().size()队列中积压的任务数。getTaskCount()历史上已提交的任务总数。getCompletedTaskCount()已完成的任务总数。可以封装一个监控工具类定时采集这些指标并上报到监控系统publicclassThreadPoolMonitor{publicstaticvoidmonitor(ThreadPoolExecutorexecutor,StringpoolName){ScheduledExecutorServiceschedulerExecutors.newSingleThreadScheduledExecutor();scheduler.scheduleAtFixedRate(()-{intactiveexecutor.getActiveCount();intpoolSizeexecutor.getPoolSize();intqueueSizeexecutor.getQueue().size();longcompletedexecutor.getCompletedTaskCount();System.out.printf([%s] active%d, poolSize%d, queueSize%d, completed%d%n,poolName,active,poolSize,queueSize,completed);},0,10,TimeUnit.SECONDS);}}5.3 拒绝策略的监控化自定义拒绝策略时除了处理拒绝逻辑还可以记录拒绝次数和任务信息方便事后排查RejectedExecutionHandlerhandler(r,executor)-{System.err.println(任务被拒绝: r.toString());// 上报到监控系统MetricsCollector.recordReject(poolName);thrownewRejectedExecutionException(线程池已满);};6. 总结线程池的配置不是一劳永逸的需要结合业务场景、任务类型和流量峰值持续调优。核心要点可以概括为核心线程数决定常驻处理能力最大线程数决定峰值弹性。队列决定任务的缓冲方式有界队列更可控。拒绝策略决定系统过载时的兜底行为要按业务可靠性要求选择。CPU 密集少配线程IO 密集多配线程。通过自定义 ThreadFactory让线程可识别通过监控指标让线程池状态可观测。只有把参数、队列、拒绝策略和监控手段结合起来才能让线程池在高峰期稳如泰山而不是动不动就「爆掉」。