线程池这玩意儿面试问烂了实际项目里也用烂了但真敢说“我完全搞懂了”的人我猜不多。尤其是那个执行顺序很多人背了答案“核心线程→队列→非核心线程→拒绝”结果一写代码发现用Executors.newFixedThreadPool配了个无界队列非核心线程压根没机会创建拒绝策略也成了摆设——顺序全对但实际跑起来完全不是一回事。所以这篇不聊虚的就掰开揉碎讲清楚线程池那几个参数到底各自管什么任务提交后到底按什么路径走完一生以及你在项目里到底该怎么配、怎么调、怎么排查问题。这篇适合谁看刚接触线程池、被各种参数绕晕的新人写了好几年代码但全靠默认配置、对内部机制还是含糊其辞的开发者以及要优化线上线程池性能、排查过队列积压或线程耗尽的老手。我尽量用大白话加实际代码把逻辑捋顺争取你看完能直接给你的线程池做一次体检。1. 七个参数逐一拆解每个参数到底是干什么的1.1 corePoolSize真正干活的“正式工”corePoolSize是核心线程数你可以把它类比成一家公司的正式员工。只要线程池还活着这几位默认就一直待在池子里哪怕没活干也不会被辞退。但有两点容易踩坑。第一核心线程不是线程池一创建就立刻拉满的而是有任务提交了才开始创建直到达到corePoolSize的数量。不过ThreadPoolExecutor提供了prestartAllCoreThreads()方法可以提前把核心线程全部拉起来适合那些任务量波动大、对首次响应时间敏感的场景。第二默认情况下核心线程闲着也不会销毁除非你调用了allowCoreThreadTimeOut(true)让核心线程在空闲超过keepAliveTime后也退出这点后面细说。1.2 maximumPoolSize临时顶上的“外包员工”最大线程数就是线程池里允许存在的线程总数上限。注意一下逻辑maximumPoolSize不是让你在corePoolSize之外再额外创建这么多而是包含核心线程在内的总上限。什么时候创建非核心线程这里就是大多数人记错顺序的地方——不是核心线程满了就立刻开新线程而是核心线程满了之后任务先往队列里塞队列也满了才会开新线程。这个“先队列后线程”的顺序是ThreadPoolExecutor和很多人的直觉最大的冲突点。我用真实场景解释一下假设corePoolSize2maximumPoolSize5队列容量3。你提交第1、2个任务核心线程立刻接手提交第3个任务核心线程都在忙于是这个任务进队列等待提交第4、5个也一样进队列直到提交第6个任务此时队列已满3个才会创建一个新线程去执行它然后第7个任务又触发创建第二个新线程最多创建到5个线程再来任务就只能走拒绝策略了。这跟很多新手的理解完全不同——总有人以为是“线程不够→开新线程→队列当缓冲区”实际上是“线程不够→队列缓冲→队列不够→再开线程”。记住这点线程池的执行顺序就理解了一半。1.3 keepAliveTime与unit空闲线程的“辞退标准”keepAliveTime是线程空闲存活时间unit是时间单位。它管的是非核心线程的空闲回收——非核心线程执行完任务后如果空闲时间超过了keepAliveTime就会被销毁防止线程数量只增不减白白占内存、占栈空间。上面提到了allowCoreThreadTimeOut(true)这个设置一旦打开核心线程也会受keepAliveTime影响空闲超时就会退出。这么做的典型场景是低峰期不想养太多线程让线程数随负载自动收缩。但要注意allowCoreThreadTimeOut一旦开启核心线程数就形同虚设线程池实际线程数可能降到0这对突发流量来说未必是好事因为线程冷启动一个需要一个时间成本。1.4 workQueue候客厅的“座位数”workQueue是任务队列也就是任务的等待区。它的选型直接决定了上面说的执行顺序会不会走到“开新线程”那一步。记住一条核心准则队列容量是有限的才有后面的非核心线程和拒绝策略队列容量无限大线程池最大线程数就成了摆设。如果你用的是无界队列比如Executors.newFixedThreadPool默认的LinkedBlockingQueue没指定容量时容量是Integer.MAX_VALUE那么队列永远不会满maximumPoolSize参数根本触发不到拒绝策略也不会触发。很多线上事故就是这么来的——线程池最大线程数设了个100但队列是无界的流量突增时任务全堆在内存里GC压力飙升响应时间暴涨而线程数始终没超过核心那几个。1.5 threadFactory线程的“出生装备”threadFactory是线程工厂用来创建线程。它虽小但直接影响问题排查的速度。默认工厂创建出来的线程名字是一串“pool-1-thread-1”这种格式线上看线程栈、做线程转储时你根本分不清哪个线程是哪个业务的。实际项目中我强烈建议自定义一个threadFactory至少把线程名改成有业务标识的形式比如“order-async-thread-1”、“report-export-thread-2”。这样万一出问题jstack一抓一眼就能定位是哪个业务模块的线程池在作妖。除了命名这里还能做点别的事比如设置daemon标识、设置线程优先级、自定义UncaughtExceptionHandler捕获线程内异常。1.6 RejectedExecutionHandler接待不下时的“下策”当线程池拉满、队列也满时新提交的任务就会被交给RejectedExecutionHandler处理。这等于告诉你你已经超负荷了接下来怎么做你定。JDK内置了四种拒绝策略每种策略对应一种态度AbortPolicy直接抛异常这是默认选项意图是“别硬撑了赶紧告诉我”DiscardPolicy直接丢弃任务不抛异常静默丢失风险最大DiscardOldestPolicy把队列里最老的任务丢掉再尝试提交新任务适合不做完也无所谓的场景CallerRunsPolicy不抛异常也不丢任务而是谁提交谁执行——也就是说提交任务的线程自己把这个活儿干了能起到天然限流的作用因为提交线程忙着干活就没法继续提交新任务了。四种策略各有各的适用场景没有绝对的好坏下面我会展开讲。2. 任务提交后一份任务在ThreadPoolExecutor里的完整旅程2.1 从execute()说起你把一个Runnable传给execute()线程池内部到底做了什么很多文章直接给你结论“核心线程→队列→非核心→拒绝”但你要真正理解得看着代码走一遍逻辑。JDK里execute方法的逻辑顺序大致是这样ctl这个字段用一个int同时记录线程池状态和线程数但流程很直观 1. 如果当前工作的线程数 corePoolSize创建一个核心线程来执行这个新任务。 2. 如果能成功将任务放进队列offer说明队列还没满那这个任务就在队列里等着。 但这里还有个细节——线程池会“复核”一次如果线程池已处于非运行状态或线程数为0了就移除这个任务并走拒绝逻辑如果线程数还是0就创建一个空转的非核心线程防止之后没有线程去执行队列里的任务。 3. 如果任务没法进队列队列满了就尝试创建一个非核心线程去执行它。 4. 如果非核心线程也创建不了线程数已经达到maximumPoolSize就走拒绝策略。注意这里面有个很容易被忽略的隐性逻辑线程池用同一个分支去处理“核心线程未满”和“非核心线程创建”的场景它内部其实是通过compareAndIncrementWorkerCount来抢线程名额的。多个线程同时提交任务时等于在抢这个名额所以“核心线程数”和“最大线程数”表达的都只是“名额上限”而不是“一定会创建这么多线程”。2.2 为什么先填队列而不是先开新线程这是整个线程池设计里最反直觉、也最关键的一个决策。你想想核心线程不够了但队列还有空位此时有两个选择A 赶紧开个新线程来顶B 让任务排队等着。ThreadPoolExecutor选了B。为什么核心逻辑是线程的创建和销毁是有成本的每个线程都要占用内存默认栈1MB左右线程切换也要开销。如果一有任务排队就开新线程那在任务量上下波动的场景里线程数量会像弹簧一样反复伸缩反而造成额外的性能损耗。让任务先排队是给系统一个“平滑缓冲”的机会等到连缓冲都扛不住了说明确实需要更多线程来消化负载了这时候才扩容。这个设计理念说白了就是线程池不是一个看见任务就抢的“暴脾气”而是一个觉得“先等等看”的“慢性子”。它默认负载会自我消解队列就是用来兜住瞬时波动的。这个顺序理解透了你就明白为什么无界队列等于废掉了扩容通道也明白为什么你在调优时最先要想清楚的就是队列容量到底该多大。2.3 换个角度队列什么时候真正“满”很多文章讲执行顺序时默认队列是满的但现实中队列“满”没那么容易。假设corePoolSize10maximumPoolSize20队列容量100你一口气提交了500个任务执行顺序是10个核心线程立刻接管10个剩下490个前100个能放进队列队列满后立即创建非核心线程去执行新来的任务也就是说第111个任务队列满之后的第1个会触发第一个非核心线程创建到第120个任务提交时非核心线程创建了10个此时线程总数到20满了第121个开始全部走拒绝策略。仔细想想这个过程你会发现队列“满”的时间点很关键它不是等所有线程都忙完了才满而是主线程提交任务的动作本身就是“一边提交、一边消费”的。任务提交速度如果大于执行速度队列才会涨如果提交速度慢了队列里原来的任务会被空闲线程消化掉队列根本满不了。这也是为什么很多线程池配了100的队列但实际队列里永远只有一两个任务——不是你的队列配大了而是你的任务提交速率本身就低于执行吞吐。3. 队列选型对比有界、无界、同步移交怎么选3.1 三种队列的本质差异workQueue选型决定了线程池对压力的处理方式。先说结论再说结论背后的逻辑LinkedBlockingQueue链表结构默认容量是Integer.MAX_VALUE也就是无界也可以手动传容量做成有界队列。ArrayBlockingQueue数组结构必须指定容量天然有界。SynchronousQueue不存任务的队列任务直接交给线程没有“排队”的中间层。这三者最大的区别在于“是否允许等待”。LinkedBlockingQueue和ArrayBlockingQueue都允许任务排队区别只是内部存储结构而SynchronousQueue压根不排队每个任务都立刻尝试交给线程。配合执行顺序来看就能明白如果用了SynchronousQueue任务进队列永远失败因为根本没有容量那基本等于“核心线程满→直接尝试创建非核心线程”。这就是Executors.newCachedThreadPool的做法它的maximumPoolSize设成Integer.MAX_VALUE可以无限创建线程。3.2 实际项目中我更推荐哪种以我在生产线上的经验做对比的话我推荐把选择逻辑拆成两层第一层你的业务能接受任务等待吗如果不能比如实时性要求极高那就用SynchronousQueue配较大的maximumPoolSize让线程池尽量保证任务被立刻执行如果能接受短暂等待就用有界队列。第二层你的业务在峰值时愿意承担什么代价如果任务可以丢弃比如日志上报、非核心统计用有界队列满了就走拒绝策略如果任务一个都不能丢比如订单状态变更队容量就得认真算拒绝策略也要选CallerRunsPolicy而不是直接抛异常。这里有个常见误区有人觉得ArrayBlockingQueue一定比LinkedBlockingQueue好因为“有界安全”。其实结合性能来看LinkedBlockingQueue因为是链表结构入队出队都只需要操作队首队尾两个节点在“队列大、并发高”场景下表现不算差ArrayBlockingQueue的数组结构在“队列小、容量固定”时效率更高。我更倾向用容量可控的LinkedBlockingQueue——比完全无界安全又比ArrayBlockingQueue灵活。我用一张表列一下对比队列类型是否有界内部结构典型使用场景注意点LinkedBlockingQueue无界否链表Executors.newFixedThreadPool默认任务无限堆积有内存风险不推荐生产直接用LinkedBlockingQueue有界是链表业务任务可排队、需要削峰填谷需为队列容量设定上限和监控告警ArrayBlockingQueue是数组容量固定、并发量高的批处理参数必填没有默认无界模式SynchronousQueue否无存储Executors.newCachedThreadPool默认必须有线程立刻处理否则反复创建线程3.3 队列容量到底怎么算这个问题没有标准答案但有个实际经验可以给你参考队列容量一般按“核心线程每秒处理能力 × 可接受最大排队时长”来估算。比如你的核心线程每秒能处理50个任务业务能接受任务在队列里等10秒那队列容量大概设在500左右。当然这只是个粗略估算实际还需要根据线上指标调整。说白了队列容量是在“等待时间”和“丢弃概率”之间做权衡容量越大任务越不容易被拒绝但排队延迟越高容量越小响应越及时但超负载时被拒绝的概率越大。没有完美的数字只有适合你业务的数字。4. 拒绝策略怎么选四种策略的真实代价4.1 四种策略的源码级理解把四种拒绝策略分开看你才会发现它们的底层逻辑指向完全不同的设计哲学。AbortPolicy是ThreadPoolExecutor默认策略它的实现就是往外面扔一个RejectedExecutionException。这是最干脆的做法意思是“我不接新活了你看着办”通常配合try-catch在业务外层捕获并降级。有人嫌它粗暴但我觉得在核心链路中用AbortPolicy反而是好事——它能第一时间暴露出容量不足的问题而不是默默吞掉。DiscardPolicy是静默丢弃实现就一行什么都补做。它的危险在于任务丢了你完全不知道。线上数据丢了你连从哪儿排查都不知道所以我基本不推荐除非丢的任务真的无足轻重。DiscardOldestPolicy的实现是从队列里poll出最老的任务丢掉再放新任务进去。从代码上看它会尝试再执行一次插入。如果这个最老的任务是核心流程的一部分丢了它的后果可比丢一个新任务严重。所以这个策略也不太被推荐只适合队列里任务的重要程度完全相同的场景。CallerRunsPolicy是我个人很欣赏的策略源码逻辑是线程池已关闭就直接丢弃任务否则调用提交任务的线程去执行这个任务。换句话说你把任务提交给了线程池线程池不干了那就你自己干。这个策略的巧妙之处在于天然限流——提交线程一旦自己开始执行任务它就没法继续疯狂提交新任务了相当于用一个反作用力把提交速度压了下来。4.2 不同业务场景下的推荐组合我整理了一份比较实用的对应关系你按业务类型对号入座业务类型推荐队列推荐拒绝策略理由订单、支付等核心操作有界队列容量较小CallerRunsPolicy不丢任务用提交线程兜底天然限流实时性较高的用户请求SynchronousQueueAbortPolicy 外层降级任务不排队满了直接提示系统过载日志、埋点、统计类异步任务有界队列容量适中DiscardPolicy丢几条不影响大局避免阻塞主流程定时批量任务有界队列容量较大AbortPolicy 监控告警确认队列容量足够异常时告警才放心这里要专门提醒一点CallerRunsPolicy在你的提交线程本身是异步线程时要格外小心。比如你是在另一个线程池的任务里再往这个线程池提交任务一旦触发CallerRunsPolicy当前这个执行线程就会被阻塞去处理任务可能会连带阻塞上游任务造成“链式拥堵”。实际调优时需要评估清楚调用链上每个环节的承载能力。4.3 要不要自定义拒绝策略内置四种策略够用吗如果你的需求是“拒绝时记录日志”、“拒绝时打点监控”、“拒绝时发送告警”那我建议直接在策略里包一层。自定义策略很简单实现RejectedExecutionHandler接口就行比如写一个LogRejectedHandler在rejectedExecution方法里记录一下任务内容、线程池状态、时间戳再做下一步动作。这个“下一步动作”到底是抛异常、丢弃还是自己执行取决于业务需求。我在项目中遇到过一种情况任务被拒绝后主流程还在等着这个任务的结果此时直接抛异常会导致上层拿到错误状态白白损失一次业务请求。后来我改成了“拒绝后先写入本地持久化队列再由单独线程重试提交”的补偿方案效果也不错。这才是自定义拒绝策略真正的价值——策略不只是一个动作而是一个完整的补偿入口。5. 手写线程池 vs Executors工厂方法别再无脑用Executors5.1 Executors四个常用方法的隐藏问题Executors类提供了几个现成的线程池工厂方法日常开发里用得很多但你必须知道每个方法背后的坑否则写出来的线程池就像一颗定时炸弹。newFixedThreadPool(n)核心线程数n最大线程数n队列是无界LinkedBlockingQueue。看上去很稳定但因为队列无界一旦提交任务速度超出处理速度任务会无限堆积内存压力越来越大。在追求稳定性的生产环境这等于把容量限制这道防线给拆了。newCachedThreadPool()核心线程数0最大线程数Integer.MAX_VALUE队列是SynchronousQueue。有任务就创建线程处理线程空闲60秒就回收。这看起来灵活但因为最大线程数无限大极端情况下可以创建成千上万个线程直接打爆内存。newSingleThreadExecutor()单线程执行任务队列无界。适合需要严格串行执行的任务。但它本质上就是newFixedThreadPool(1)同样存在无界队列的问题。newScheduledThreadPool(n)支持定时和周期任务内部用的是DelayedWorkQueue这个队列无界也有同样的隐患。这几行青春版工厂方法适合快速写demo但生产代码里我更推荐全部使用ThreadPoolExecutor手动创建把每个参数都明明白白写出来哪怕加注释解释来源都行也不要把责任交给一个你不清楚内部实现的方法。5.2 一个可以直接抄作业的手写配置模板下面这个配置是我在实际项目中用过比较稳的一个模板你拿去套用的时候记得根据业务调整参数ThreadPoolExecutor taskExecutor new ThreadPoolExecutor( 10, // 核心线程数按机器核数和任务类型定 20, // 最大线程数一般为核数的2倍左右 60, // 空闲回收时间 TimeUnit.SECONDS, // 时间单位 new LinkedBlockingQueue(500), // 有界队列容量按峰值流量估算 new ThreadFactory() { private final AtomicInteger cnt new AtomicInteger(0); Override public Thread newThread(Runnable r) { Thread t new Thread(r, biz-common-task- cnt.incrementAndGet()); t.setDaemon(false); return t; } }, new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝时让提交线程执行 );我强调几点第一线程名一定要带上业务前缀线上查问题全靠它第二拒绝策略按前面说的场景选别默认AbortPolicy万事大吉第三有界队列容量要配合监控使用后文我会讲怎么定这个数。顺便也提一下最佳实践如果细心你还会给这个线程池加一个“是否优雅关闭”的钩子比如在应用关闭时调用shutdown()并awaitTermination等待已提交任务执行完。很多线上任务丢失的故障都出在应用重启时直接终止了线程池没有给线程池留出过渡时间。6. 线程池调优实战场从指标观察到参数调整6.1 先看哪些指标线程池的“体检报告”调优不能靠猜先要能拿到数据。ThreadPoolExecutor提供了几个可以从外部观测的指标getPoolSize()当前线程数getActiveCount()当前活跃线程数即正在干活的数量getQueue().size()当前队列积压量getTaskCount()任务总量getCompletedTaskCount()已完成任务量。把这些指标定期采样打点就能看到线程池的真实运行状态。最直接的一条规律如果getActiveCount长期跟getPoolSize()一样高且队列积压量还在涨说明线程池已经顶不住了要考虑扩容或拒绝新的请求如果getPoolSize()长期大于getActiveCount()说明线程多了可以考虑调小maximumPoolSize或降低keepAliveTime让空闲线程更早回收。6.2 核心线程数到底怎么定网上流传的“CPU密集型设核数1IO密集型设核数×2”是简化版经验实际上还需要参考等待比例网上也有各种各样的公式。但老实说真实项目里谁也不会因为你配了个按公式算出来的线程数就万事大吉还要结合压测数据来验证。我个人更实际的做法是这样CPU密集型任务设置成CPU核数或核数1避免线程太多互相抢CPU时间片。IO密集型任务因为线程大部分时间在等待IO可以多设通常为核数的1.5倍到3倍。混合型任务把IO部分和CPU部分拆开分别用不同线程池处理。最终以压测结果为准。我每次调线程池都先按经验值配好再做一次流量摸底压测看吞吐和延迟曲线找到拐点再微调。如果你不会估算我建议先用比较少的核心线程数配合有界队列跑起来观察队列积压和拒绝次数让数据告诉你该往哪个方向调比一开始就给一个“看起来很大”的数字靠谱得多。6.3 动态调整线程池参数线程池的一个很好用的特性是运行期间可以动态调整参数。setCorePoolSize()、setMaximumPoolSize()、setKeepAliveTime()都能操作而且线程池内部做了并发控制不会让你改完参数产生冲突。但要注意一点如果新设置了corePoolSize大于当前的线程数线程池并不会立刻补建线程而是要等到有新任务提交时才会逐渐补充如果setCorePoolSize小于当前线程数多余的线程会在它们跑完当前任务后陆续退出。也就是说参数调整不是瞬间生效的你需要留出一段缓冲时间观察效果不要刚改完就看到线程数没变化就以为调整无效。7. 高频问题排查实录线上线程池踩坑与解决7.1 现象一任务堆积但线程数就是上不去遇到这个问题的第一反应要落在队列类型上。如果你用了无界队列那恭喜一切的锅都在这队列不会满线程池永远不会触发非核心线程创建线程数自然始终等于核心线程数。我见过一个案例某系统用Executors.newFixedThreadPool(20)处理大量业务请求结果并发一高队列里积压了几千个任务但线程数始终是20就是无界队列这个“隐形杀手”。排查方法很简单查看线程池初始化代码看workQueue用的是不是无界队列再看监控里的queue.size如果积压持续增长而poolSize不变基本可以实锤。7.2 现象二任务被拒绝但明明线程数没到最大值这个现象经常让人百思不解但你仔细推演一遍就能发现所有被拒绝一定是因为线程数已经到达了maximumPoolSize虽然当时活跃线程不一定等于最大值——非核心线程在执行完任务后可能刚好陷入空闲但还没超过keepAliveTime被回收此时线程总数依然等于maximumPoolSize新任务再来一样被拒。所以排查这种问题不能只看“线程是否都在忙”要看“线程总数是否已经拉满”。还有一个影响因素是提交线程非常快瞬间把队列填满进而触发扩容即使任务很快被执行完也会出现线程峰值。这时候我们要做的不是拒绝而是控制提交速率。7.3 现象三用了CallerRunsPolicy线上延迟飙高CallerRunsPolicy确实能兜底不丢任务但它有个隐蔽的坑如果提交任务的主线程是业务请求线程一旦触发拒绝策略这个请求线程就会挂在任务上执行这会导致请求响应时间变长严重的时候会连累整个请求入口让延迟曲线突然拉出长尾巴。我处理过类似线上事故后来把策略做了改进不是让提交线程执行而是先把任务放到一个容量可控的备用队列再开一个补偿线程去慢慢消化。效果比直接让业务线程“硬扛”温和得多。7.4 现象四线程数降不下来内存增长异常线程池的线程空闲后按keepAliveTime销毁但只针对非核心线程。如果你没开allowCoreThreadTimeOut核心线程是永远不会销毁的。如果你的线程池创建了太多非核心线程而每个线程又占着栈内存内存自然下不来。排查这种问题要看线程池的maxPoolSize是不是配得过大以及是否用了SynchronousQueue导致线程反复创建。另一种可能是线程池线程执行的任务里有阻塞操作比如网络等待、锁等待线程被阻塞自然没法进入空闲回收流程。此时你真正要解决的是任务里的阻塞问题而不是调整线程池参数。7.5 现象五线程池死锁任务全部卡住线程池里也会死锁而且排查起来比同步代码块死锁更隐蔽。典型的场景你把一个大任务拆成多个子任务丢给同一个线程池执行然后这个任务内部又wait子任务的结果。子任务虽然在线程池里排队但父任务占用了线程池里所有核心线程在等待子任务永远拿不到线程执行整个线程池就“假死”了。解决办法有几个方向一是父任务和子任务用不同的线程池打破依赖循环二是不要在线程池任务内部同步等待另一个任务的结果三是如果用Future.get()必须设置超时时间。这个坑踩过的人不少线上线索就是你发现线程池里有几个线程一直处于WAITING状态但所有任务都不推进。8. 监控与优雅关闭线程池的日常运维细节8.1 线程池监控到底监控什么我建议每个线程池都配上监控至少包含这几个维度活跃线程数、队列深度、任务提交速率、任务执行耗时、拒绝次数。拒绝次数是最直观的报警信号只要出现了拒绝就说明容量已经到顶了这时候无论是扩容还是降级都要立刻有动作。具体实现不需要太复杂在任务包装层统一加一条监控逻辑即可提交时记录一个开始标记执行完再记录耗时定时打点到监控系统。如果你的团队用的是Spring也可以把ThreadPoolTaskExecutor包一层然后注入在execute方法前后统一打上指标。8.2 shutdown与shutdownNow动静有别线程池关闭有两个方法shutdown()会等待所有已提交任务执行完再关闭这是推荐的方式shutdownNow()会尝试立即停止所有活动任务并返回未执行的任务列表不推荐在生产环境直接调用因为它可能打断正在执行的任务。线上优雅关闭的写法一般是先调shutdown()然后awaitTermination一段时间如果超时还没结束再考虑强制关闭。不过强制关闭也不是shutdownNow这么简单通常要先暂停接收新任务、发出告警、再逐步杀掉。这里有个容易被忽略的地方Spring容器销毁Bean时线程池Bean如果没实现DisposableBean线程池不会自动关闭容器关闭时就会漏掉线程。如果你在Spring项目里用了线程池一定要确保执行了shutdown逻辑否则每次重启都会留下孤儿线程。8.3 一个简单的关闭代码模板executor.shutdown(); try { if (!executor.awaitTermination(30, TimeUnit.SECONDS)) { // 超时未结束记录日志并强制关闭 executor.shutdownNow(); if (!executor.awaitTermination(30, TimeUnit.SECONDS)) { // 强行关闭也没成功任务确实卡死了 System.err.println(Thread pool did not terminate); } } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); }实话说线程池的配置没有一个万能数字它跟你的业务类型、机器配置、任务特性强相关。我个人在实践中最深刻的一点体会是线程池调优本质上是在排队等待和拒绝丢弃之间找一个业务的平衡点而且这个平衡点会随流量变化而移动。所以我现在的习惯是把参数写进配置中心让它在不同时段可以动态调整再配上监控告警而不是拍脑袋定一个数字就再也不动。另外一个值得分享的小技巧压测的时候先把最大线程数和队列容量压到非常激进的值比如线程数2队列容量2观察拒绝策略是否按预期触发确认流程没问题后再逐步放开到合理值。这样既验证了你的拒绝策略也让你真正理解了各类参数之间的联系。希望这篇把线程池的参数和执行顺序讲透了。如果你在项目里配线程池还有什么拿不准的或者遇到过我没列出来的奇怪问题欢迎按这个思路自己动手推演一遍——排查线程池问题最忌讳的就是凭感觉猜你把执行顺序在纸上画一遍其实大部分问题就已经有了答案。