先聊点实际的。线程池这仨字搞 Java 的人几乎天天见面试背八股要背线上排查故障要查但真正能把原理讲清楚、又能把参数填对的人我遇到的真不多。我见过太多团队直接把 newFixedThreadPool(10) 往代码里一扔然后生产环境任务堆到内存报警还不知道是线程池的问题也见过有人为了优化性能把最大线程数调到几百结果接口反而更慢了。这篇文章我想把线程池的原理和应用放到一起讲透。原理部分不会只停留在“ corePoolSize 是核心线程数”这种表面解释而是把任务进来之后到底走了哪条路、队列和最大线程数的关系是什么、为什么拒绝策略不能随便选这些全部拆开。应用部分全部是能落地的参数怎么算、线程池怎么隔离、怎么监控、怎么优雅停机包括我自己在项目里踩过的坑。适合刚接触并发编程的初学者也适合两三年经验、想把自己的线程池配置从“凭感觉”变成“有依据”的开发。1. 线程池到底解决了什么问题1.1 线程创建和销毁的成本很多人对线程的理解还停留在“ new Thread().start() 挺简单的嘛”确实写起来简单但代价不简单。创建一个线程操作系统要分配线程控制块TCB、分配内核栈和用户栈空间、完成调度器的注册默认情况下一个线程的栈空间就有 1MB 左右虽然不是一开始全部分配物理内存但是虚拟内存和内核数据结构都是要占的。销毁线程的时候这些资源又要一一释放。这些操作的成本和一次字符串拼接、一次 HashMap 查询完全不是一个量级。如果你的系统是在高并发下短时间来了几千个请求每个请求都开一个线程做完再销毁你会发现 CPU 大量时间花在线程的创建和销毁上真正干活的时间反而被挤掉了。这就是池化技术出现的原因——和数据库连接池、对象池一样把创建和释放的成本摊到复用里去。1.2 并发失控带来的连锁反应不用线程池、每请求一线程的模式还有个更致命的问题并发量不可控。操作系统能够同时运行的线程数是有限的线程多了之后CPU 调度器要不停地在各个线程之间切换。上下文切换本身要保存寄存器状态、程序计数器、内存映射信息这些操作全是开销。我在一个压测场景里见过一个真实案例某接口用每请求一线程的方式QPS 拉高到 1000 时线程数直接冲到 3000 多CPU 使用率看着不高但接口响应时间从 50ms 飙到了 800ms。原因就是大量 CPU 时间都花在上下文切换上了线程之间都在抢 CPU谁都没法好好干活。另外线程数量过多还会消耗内存。每线程默认 1MB 栈的虚拟内存3000 个线程就是 3GB 的虚拟地址空间再加上线程内核结构、任务队列等很容易把内存吃紧。线程池从根上解决了这个问题——把并发执行的任务数量限制在一个可控范围内让系统始终处于健康的工作状态。1.3 池化思想同一种方案的共性线程池的核心思想不只在 Java 里有。数据库连接池、HTTP连接池、内存对象池原理都是一样的预先创建一批资源放着用的时候从池子里取用完了还回去不够了再按规则扩容太多了按规则回收。池化要解决的核心问题就是两个一个是降低资源创建销毁的开销一个是限制资源使用的上限。用个生活化的类比。餐厅后厨如果每个客人来了才临时招厨师菜炒完了把厨师开除饭店肯定开不下去。正常做法是提前养几个固定厨师核心线程客人多了排队等位任务队列等位区满了就临时加人扩容到最大线程数实在接待不了就告诉客人今天满了拒绝策略淡季了临时工再辞退线程回收。线程池就是这套餐厅管理的翻版理解了这套模型你再看源码里的每个参数都会觉得眼熟。2. ThreadPoolExecutor 核心原理拆解2.1 七个构造参数每一个都不能糊弄Java 里线程池的标准实现是 ThreadPoolExecutor最完整的构造器有七个参数。我建议你直接把这个构造器背下来因为所有的坑几乎都藏在这些参数里corePoolSize核心线程数线程池常驻的线程数量即使空闲也不会销毁除非设置了 allowCoreThreadTimeOut。maximumPoolSize最大线程数线程池最多能创建的线程数量。keepAliveTime非核心线程的空闲存活时间也就是线程空闲多久之后被回收。unitkeepAliveTime 的时间单位。workQueue任务队列核心线程都忙时新任务先进入队列排队。threadFactory线程工厂用来创建线程可以自定义线程名、优先级等。handler拒绝策略当队列满且线程数达到最大时新任务如何处理。很多人的误区是把 corePoolSize 和 maximumPoolSize 当成“线程数量的范围”这个理解方向是对的但不够精确。精确的理解是corePoolSize 是正常情况下维护的线程数maximumPoolSize 是在队列满了、任务积压时的“拯救措施”。两者之间的差值是“应急线程”的数量。2.2 任务提交后的四条路径这是线程池原理里最核心的部分。当调用 execute(command) 提交一个任务时它按照下面的顺序判断如果当前工作线程数小于 corePoolSize直接创建一个新线程来执行这个任务哪怕此时有核心线程正处于空闲状态也会优先创建新线程。如果当前工作线程数已经达到 corePoolSize尝试将任务放入 workQueue 队列排队。如果队列已经满了继续判断当前工作线程数是否小于 maximumPoolSize如果小于就创建新的非核心线程来执行任务。如果线程数已经达到 maximumPoolSize队列也满了就交给拒绝策略处理。这里有个反直觉的点很多人搞错是先排队再扩容而不是先扩容再排队。从刚入门那会儿我就在想任务多了不应该马上开新线程吗为什么先塞队列后来想通了线程创建是有成本的如果任务只是短时间内的波动让任务在队列里等一等等核心线程处理完手上的任务再来取比大动干戈创建新线程要划算得多。只有队列这个缓冲带也填满了说明任务确实积压了这时候才值得使用权当应急手段的非核心线程。这个顺序带来一个非常重要的推论如果你用的是无界队列比如没有指定容量的 LinkedBlockingQueue那么第 3 步永远不会触发maximumPoolSize 这个参数就形同虚设了。你设了最大线程数 100实际永远只会有 corePoolSize 个线程在跑其他任务全部堆积在队列里。2.3 工作队列选型边界就是生命线workQueue 是线程池里容易被轻视的参数它决定了任务积压时的去向。常见的几种LinkedBlockingQueue如果不指定容量默认是 Integer.MAX_VALUE也就是无界队列。Executors.newFixedThreadPool 用的就是这种队列任务可以无限堆积线程数固定。问题很明显任务堆积不设上限内存迟早耗尽。ArrayBlockingQueue有界队列必须指定容量这是生产环境的首选。任务积压的上限是可控的。SynchronousQueue不存储任务的队列offer 一个任务必须等待另一个线程来取否则失败。CachedThreadPool 用的就是它这种情况下线程数会很容易被顶到 maximumPoolSize。PriorityBlockingQueue优先级队列任务可以按优先级执行但注意它会破坏 FIFO 的顺序性使用前要想清楚业务是否接受。选型建议非常明确核心业务一律用有界队列容量根据业务的峰值积压量来定。不能用无界队列来掩盖流量洪峰因为你根本不知道洪峰能到多大与其让系统挂着大量未完成任务慢慢走向 OOM不如在队列满的时候触发拒绝策略让上层感知到压力、做降级处理。2.4 拒绝策略默认策略和自定义策略当队列满了、线程数也到上限了新来的任务交给 handler 处理。JDK 提供了四种策略行为使用场景AbortPolicy直接抛出 RejectedExecutionException默认策略适合对任务丢失敏感、必须告警的场景CallerRunsPolicy由调用者线程直接执行该任务不希望任务丢失同时利用调用者执行来天然限流DiscardPolicy静默丢弃任务允许任务丢失的次要业务DiscardOldestPolicy丢弃队列中等待最久的任务再尝试提交当前任务实时性要求高、旧任务价值低四种内置策略里我最推荐 CallerRunsPolicy 的场景化应用它把无法塞进线程池的任务交给调用者线程去执行等于把压力传回了上游。如果调用者是业务线程业务线程被任务卡住了它就没法继续往线程池提交新任务相当于按住了请求的入口形成一种自然限流。但在实际项目里我更倾向自定义拒绝策略而不是直接用内置的。比如记录一条告警日志、把任务写入本地文件或消息队列做补偿、通知监控系统。拒绝策略绝不是简单抛弃任务拉倒而是要和你的降级链路配合否则就是网上说的“静默丢消息”等业务出问题了你都不知道丢在哪儿了。3. 参数设计从理论到生产可用的配置3.1 corePoolSize 的计算公式线程池参数设计的核心是 corePoolSize。很多人直接抄“CPU 密集 N1IO 密集 2N”这个说法简单作为起步可以但不够准确。我推荐用下面这个更通用的公式线程数 Ncpu * Ucpu * (1 W / C)NcpuCPU 核数Ucpu期望的 CPU 利用率通常设为 1 表示希望跑满W任务的等待时间比如等待 IO 返回、等待网络响应C任务的计算时间真正使用 CPU 的时间来拿一个实际接口算一下。假设部署机器是 4 核目标 CPU 利用率 80%接口逻辑里数据库查询和远程调用占 100ms 的等待时间本地计算和序列化占 20ms。代入公式4 * 0.8 * (1 100/20) 19.2约等于 20。这个公式背后的道理也简单等待时间里线程相当于“休眠”不占 CPU所以可以用更多的线程去填补等待期让 CPU 尽量忙起来而计算时间越长说明线程本身在独享 CPU线程多了反而徒增切换开销。CPU 密集型任务公式退化为线程数约等于核数。我通常取 Ncpu 1多留一个线程去处理偶尔的页面缺页中断或者 GC 停顿不至于让 CPU 完全空转。IO 密集型的任务则要看等待和计算的比例并不是简单地翻倍。如果一次请求 90% 时间都在等网络线程数是核数的 10 倍都有可能具体比例以压测为准。3.2 maximumPoolSize 和 keepAliveTime 的权衡maximumPoolSize 设多大是个取舍问题。设得小兜底能力弱流量一冲就触发拒绝策略设得大线程数膨胀后上下文切换开销增加系统反而变慢。我的经验是maximumPoolSize 设置为 corePoolSize 的 2 到 4 倍具体看任务对延迟的敏感度。如果是延迟敏感型接口maximumPoolSize 可以相对大一点用线程数换处理速度如果是后台批量任务不需要那么激进核心线程加上队列容量已经能够平滑处理大多数情况。keepAliveTime 默认只对非核心线程生效线程空闲超过这个时间才会被回收。这个值设多长主要看流量的周期性。如果业务有明显的高峰低谷比如早晚高峰或每小时的整点keepAliveTime 可以放宽到几分钟让应急线程在低谷期也能存活一段时间避免下一次高峰到来时还要临时建线程如果是平稳流量30 到 60 秒就够了。这里提一个容易被忽略的点核心线程默认是不会被回收的哪怕空闲时间再长。如果想让核心线程在空闲后也释放需要显式调用 allowCoreThreadTimeOut(true)同时 keepAliveTime 必须大于 0。这个配置在资源紧张的容器环境里很有用但要注意它改变了“核心线程常驻”的语义线程池可能缩到 0 个线程下次任务来了再创建会有一定的延迟。3.3 队列容量的配合队列容量和线程数是一体的不能单独拍脑袋。我的切入思路是算最大积压量。假设一个订单系统的处理线程池核心线程 8单任务平均处理 50ms那么在 1 秒内能处理约 160 个任务。如果峰值流量是每秒 1000 个请求持续 10 秒减去线程能处理的 1600 个积压任务约为 8400 个。那么队列容量至少设 8400 才有兜底空间。但队列容量不是越大越好。队列排队的任务都是有生存时间的如果一个请求在队列里等太久前端早就超时了这时候任务即使再被执行也没有意义反而拖着系统的后腿。所以更合理的做法是为队列里的任务设置一个“最大在队等待时间”的概念——用队列容量除以每秒的处理能力估算出队头任务的等待时间。长于业务允许的延迟上限时这个队列容量就是过量的应该把容量降下来把多余的负载交给拒绝策略去降级。3.4 业务隔离一套线程池只干一件事很多系统只有一个全局线程池所有业务共用这在低并发时看不出来高并发时就是灾难。某个后台报表任务突然阻塞把线程池的线程全占满了前端的下单接口去同一个池子里拿不到线程也跟着超时。这就是线程池“互相污染”。我现在工作中一定会做线程池隔离至少做到按业务域拆分。给你看一个参考配置业务场景核心线程数最大线程数队列类型与容量拒绝策略订单处理816ArrayBlockingQueue(1000)CallerRunsPolicy消息推送24ArrayBlockingQueue(200)DiscardOldestPolicy报表计算48ArrayBlockingQueue(500)AbortPolicy 告警审计日志12ArrayBlockingQueue(5000)DiscardPolicy订单处理是核心链路拒绝策略选择 CallerRunsPolicy让调用者线程兜底执行保证关键任务不丢报表计算可以抛异常告警因为这是离线任务宁可让它失败重试也不能静默丢弃审计日志允许丢弃丢了审计日志最多影响追溯不能让日志写入拖垮主业务消息推送实时性要求高但一定程度上允许丢旧任务所以用 DiscardOldestPolicy 保证最新的推送优先。隔离的代价是线程池变多线程占用变多所以不建议切得太碎。按照“核心链路 / 非核心异步 / 定时任务”这几个粒度来划分就够了别一个接口一个池子那样反而浪费资源。4. 生产级线程池的落地实战4.1 线程工厂让线程名会说话线程池创建线程默认用的是 Executors.defaultThreadFactory生成的线程名是 pool-1-thread-1 这种排查问题的时候根本分不清是哪个业务模块的线程。我见过一次线上事故jstack 抓下来几百个线程全叫 pool-xxx-thread-y完全没法定位来源。从那之后我把线程工厂这步当成强制要求每个线程池都用自定义 ThreadFactory 命名格式是“业务名-池类型-序号”。ThreadFactory namedThreadFactory new ThreadFactory() { private final AtomicInteger seq new AtomicInteger(0); Override public Thread newThread(Runnable r) { Thread t new Thread(r); t.setName(order-handler-pool- seq.getAndIncrement()); t.setDaemon(false); return t; } };命名实际上是在给自己留后路。线上出问题jstack 一拉看到 order-handler-pool 的线程全都阻塞在某处马上就能定位到对应的线程池实例排查路径会短很多。再配合 setUncaughtExceptionHandler 设置线程的未捕获异常处理器至少能保证线程跑任务时抛出的异常不会被静默吞掉。4.2 监控线程池的健康体检线程池默认是没有监控的你只能通过 getPoolSize()、getActiveCount()、getQueue().size()、getTaskCount()、getCompletedTaskCount() 这些方法在外部查看状态。这些接口是现成的但单独调用只是拿到一个瞬时的快照意义不大。我的做法是做一个定时打点任务每 30 秒采集一次线程池的关键指标输出到日志或监控平台当前线程数、活跃线程数判断池子的负载情况。队列积压数量队列持续增长说明消费速度跟不上生产速度是告警的重要信号。完成任务总数与拒绝任务总数拒绝数从 0 变为非 0说明线程池已经到瓶颈了需要立刻关注。更细粒度的监控可以重写 ThreadPoolExecutor 的 beforeExecute、afterExecute 和 terminated 方法。beforeExecute / afterExecute 这对钩子可以在任务执行前后记录时间算出每个任务的平均执行耗时和最大耗时及时发现慢任务terminated 方法在线程池关闭时回调可以用来确认线程池是否成功终止避免应用停机时线程池还挂着线程导致的资源泄漏。我自己在项目里踩过一次这种坑线程池中的任务偶发超时从外部指标完全看不出来后来通过重写 afterExecute 统计每次任务的执行时间才发现有少量任务在线程池里等待了很久才被执行队列积压才是根因。监控做得越细问题出现时定位越快这是实打实的经验。4.3 动态调参与优雅停机线程池的 corePoolSize 和 maximumPoolSize 不是不能改的。ThreadPoolExecutor 提供了 setCorePoolSize、setMaximumPoolSize 方法可以在运行期动态调整。这套机制的价值在于你不需要为了调参停机重启根据实时压测指标在线把核心线程数往上调观察一段时间再决定是否继续加。还有两个方法值得注意。prestartAllCoreThreads() 可以在初始化时把所有核心线程提前创建好避免第一个任务到达时才临时创建线程带来的响应延迟allowCoreThreadTimeOut(true) 可以让核心线程在空闲时也被回收适用于资源敏感且流量波动大的场景。优雅停机是我特别想强调的一点。直接调用 shutdownNow() 会中断所有线程正在执行的任务被打断队列里还没执行的任务会丢失这在生产环境里极其危险。标准的停机流程是调用 shutdown()线程池不再接收新任务但会继续处理队列中已有的任务。调用 awaitTermination(timeout, unit)阻塞等待线程池进入终止状态超时时间内如果没结束再进入下一步。超时后还没结束再调用 shutdownNow() 强制中断同时拿到未执行的任务列表做补偿处理。这套流程既照顾了优雅性又保证了及时性。应用发布或者缩容的时候如果不做优雅停机正在处理中的订单、正在写的日志都可能半途而废。这块儿真的是上线发布前就写好的保命符千万别省。4.4 一个可直接参考的生产配置示例把前面说的整合起来我给你一个完整的线程池创建代码。这是一个订单处理场景的线程池核心线程 8最大线程 16队列容量 1000拒绝策略用 CallerRunsPolicy加了自定义线程工厂和监控钩子。ThreadPoolExecutor orderPool new ThreadPoolExecutor( 8, 16, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(1000), namedThreadFactory, new ThreadPoolExecutor.CallerRunsPolicy() ) { Override protected void beforeExecute(Thread t, Runnable r) { long startTime System.currentTimeMillis(); // 可以把 startTime 放在 ThreadLocal 里afterExecute 中计算耗时 super.beforeExecute(t, r); } Override protected void afterExecute(Runnable r, Throwable t) { // 统计执行耗时、记录异常 super.afterExecute(r, t); } }; orderPool.prestartAllCoreThreads();注意构造器里的 7 个参数顺序别写错最后一个 CallerRunsPolicy 是重试策略当队列满、线程数满时任务会被提交任务的线程自己执行。这样配置之后这个线程池是“有名字、有边界、有兜底、有监控”的跑进生产环境至少不会两眼一抹黑。5. 手写迷你线程池把原理刻进脑子里5.1 设计思路往最小模型里塞什么理解线程池最好的方式是自己写一个能跑的最小版本。不需要像 JDK 那样支持各种状态流转和复杂参数只需要保留最核心的模型核心线程 任务队列 拒绝策略。我先想清楚这个迷你线程池要具备什么能提交任务能限制线程数量核心线程执行完任务后从队列里取下一个任务队列满时执行某个拒绝逻辑。这个模型足够表达线程池的本质了。5.2 核心实现代码下面是我写的一个简化版迷你线程池只保留了三种核心元素public class MiniThreadPool { private final BlockingQueueRunnable workQueue; private final ListWorker workers new ArrayList(); private volatile boolean running true; public MiniThreadPool(int coreSize, BlockingQueueRunnable queue, RejectedExecutionHandler handler) { this.workQueue queue; for (int i 0; i coreSize; i) { Worker worker new Worker(mini-thread- i); workers.add(worker); worker.start(); } } public void execute(Runnable task) { if (!running) { return; } if (!workQueue.offer(task)) { // 队列满了这里就相当于拒绝策略的触发点 handler.rejectedExecution(task, this); } } public void shutdown() { running false; for (Worker worker : workers) { worker.interrupt(); } } class Worker extends Thread { public Worker(String name) { super(name); } Override public void run() { while (running || !workQueue.isEmpty()) { try { Runnable task workQueue.take(); task.run(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } } } }基础的使用方式是这样的MiniThreadPool pool new MiniThreadPool(2, new ArrayBlockingQueue(10), (r, p) - System.out.println(task rejected: r)); for (int i 0; i 20; i) { int taskNo i; pool.execute(() - System.out.println(Thread.currentThread().getName() execute task- taskNo)); } pool.shutdown();5.3 与 JDK 线程池的对比和收获这个迷你版本和真正的 ThreadPoolExecutor 比起来缺了什么正好就是线程池原理中容易忽略的部分。第一没有核心线程数到最大线程数的动态扩容逻辑。我的版本里核心线程就是全部线程队列满了直接拒绝没有最大线程数这个兜底层级。而 JDK 版会在队列满时把线程数从 core 扩到 maximum让真正的 ThreadPoolExecutor 拥有了更强的弹性。第二没有 keepAliveTime 回收逻辑。JDK 版里非核心线程空闲超过指定时间会被回收迷你版里所有线程一旦创建就永生。实际系统中线程长期空闲占用栈内存也是一种浪费。第三没有状态流转。JDK 的线程池内部有 RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED 这些状态shutdown 和 shutdownNow 的行为差异很大。迷你版里 shutdown 只是简单地把 running 置 false 并中断线程细节上非常粗糙。第四没有 beforeExecute / afterExecute 钩子也没有 ThreadFactory 的定制入口。JDK 版的每一次线程创建、执行任务前后都有扩展点这些扩展点才让生产环境的监控、排查变得可能。写一遍迷你版再回来看 ThreadPoolExecutor你会清楚很多。它本质上就是一个任务队列 一组线程 一套状态管理 一堆扩展钩子的组合。抓住主干其他都是枝节。6. 高频坑位与排查记录6.1 无界队列让任务堆积到 OOM这是 Executors.newFixedThreadPool 最容易踩的坑。固定线程数搭配无界 LinkedBlockingQueue看起来稳如泰山实际上是慢性毒药。任务处理不过来时全部堆在队列里队列容量差不多是 21 亿内存一点点被吃掉直到 OOM 或者触发 GC 告警。我排查过的一个案例一个消息消费服务业务方用 newFixedThreadPool(8) 处理推送任务。某天上游消息量暴涨消费速度跟不上队列里积压了上百万个任务对象每个任务又持有一份业务数据内存直接被打满。这事的教训就是凡是可能面临突发流量的线程池一律用有界队列并且拒绝策略要配合告警让积压能够在可控范围暴露而不是等到内存爆了才发现。6.2 ThreadLocal 值串号线程池复用的不只是线程对象还有线程上的 ThreadLocal 数据。我见过一个非常隐蔽的 bug一个请求上下文里存了 traceId这个上下文用 ThreadLocal 保存任务在执行时把 traceId 写回了日志。有一天排查日志发现同一时刻出现了大量不同请求的 traceId 交替出现原因就是线程池线程在执行完上一个任务后ThreadLocal 里的 traceId 没有被清理下一个任务拿到了上一个任务的上下文。解决办法有两个层面。最基本的是在任务执行结束的 finally 块里显式调用 remove()try { // 任务逻辑 } finally { contextThreadLocal.remove(); }更高一层是做一个 Runnable 的包装器统一在 finally 里清理所有 ThreadLocal不让每个任务自己去写清理代码。如果是需要父线程向子线程传递上下文的场景就要引入值传递机制在执行任务前把父线程的上下文拷贝给子线程执行完再清理掉。线程池加 ThreadLocal 的场景永远要记住线程会复用脏数据也会复用。6.3 静默丢弃任务DiscardPolicy 这个拒绝策略用起来最轻松后果也最容易被忽视。我见过有人为了图省事给所有线程池都用 DiscardPolicy结果某个对账任务高峰期被静默丢弃当天对账不齐排查了很久才发现是线程池丢的任务。这类问题难就难在它完全无感日志里没有异常业务表面看起来也没问题唯一的表现就是某个结果不对。所以我的原则是核心业务永远不要用 DiscardPolicy 和 DiscardOldestPolicy。即使要用 DiscardOldestPolicy也要在自定义 handler 里补一条 WARN 日志。丢任务可以接受但丢得明明白白才叫策略丢得不知不觉那就是事故。6.4 线程池饥饿的排查线程池饥饿和死锁不一样线程池里的线程可能并没有卡死而是执行的任务本身太慢把线程都占住了。有一个典型的排查路径调 jstack 看线程栈如果大量工作线程阻塞在某个远程调用上或者执行某个慢 SQL说明瓶颈在于任务本身线程池只是被牵连了。还有一种饥饿是线程池层级互相依赖造成的。线程池 A 的任务要等线程池 B 的任务完成B 的队列却满了、线程都忙着A 的线程全部阻塞在等待上。这种时候看任何一个池子都是满的其实是池子之间的协作关系设计不合理。排查思路是先画出任务依赖关系图看有没有线程池之间的同步等待链有的话要么拆流程要么把有依赖关系的任务放到同一个池子里。6.5 常见问题速查表现象可能原因排查手段内存持续增长后 OOM队列无界任务积压检查 workQueue 类型观察队列 size线程数很多但 CPU 不高大量线程阻塞等待 IOjstack 查看线程栈统计 BLOCKED/WAITING任务偶尔丢失拒绝策略静默丢弃检查 handler 类型统计拒绝次数traceId 串数据ThreadLocal 未清理检查任务执行完是否有 remove线程池线程全部占满任务执行时间过长afterExecute 统计任务耗时定位耗时任务任务延迟执行队列积压线程处理不过来观察队列大小与活跃线程数变化曲线上面这些坑我基本都在生产环境里实际遇到过。线程池本身不是银弹用对了是利器用错了是雷区核心还是对原理的理解和参数的精心设计。最后说一个我自己的习惯。我每新建一个线程池都会顺手做两件事一是至少加一条创建时打印参数的日志二是确认这个池子有命名、有监控、有拒绝告警。这三样东西在系统规模小的时候看不出价值但流量一旦上来、故障一旦发生它们就是你从混乱中快速脱身的救命通道。很多人把线程池当工具用了就完了我建议你把它当成系统里的一个独立组件去运营和维护。原理搞清楚参数算明白问题盯得住这个组件才能真正让你省心。