最近被好几个群里的同学问到线程池的执行流程这个问题确实值得好好讲一讲。作为Java并发编程里出场率最高的组件之一线程池可以说是一个“你天天用但未必真懂它”的存在。无论是你把任务直接丢给ExecutorService还是在Spring里配置ThreadPoolTaskExecutor背后真正干活的都是ThreadPoolExecutor。很多人背过八股文知道线程池有核心线程数、最大线程数、阻塞队列这些概念可真要问一句“任务来了到底先走哪一步队列满了又怎么处理”能答清楚的人其实不多。更别说线上出了问题线程数飙升、队列堆积、任务被拒绝这些现象背后到底发生了什么如果对执行流程没有一个清晰的认知排查起来就会非常吃力。这篇文章我就从线程池的执行流程入手把七个核心参数、任务提交的完整路径、阻塞队列的选型、execute和submit的区别以及我踩过的一些坑一次讲清楚。内容会尽量贴合真实场景方便你在面试或者实际排障时直接拿出来用。1. 线程池到底在解决什么问题1.1 从一次异步下单说起假设你负责一个电商系统用户在下单之后后端需要做一系列操作扣减库存、生成订单、发送短信通知、推送物流信息等。如果这些事情全部同步执行接口响应时间会被短信发送这种慢操作拖得很长。于是你自然会想到用多线程把短信通知、物流推送这种非核心操作丢到后台异步执行。在没有线程池的“原始时代”很多人会这样写代码for (MessageTask task : tasks) { new Thread(() - task.send()).start(); }这样写行不行小流量的时候没问题但一旦流量上来问题就非常明显了每个请求都创建一个新线程线程的创建和销毁本身就是开销而且JVM可创建的线程数量受内存限制并发一高先是频繁GC然后直接OutOfMemoryError甚至把整个应用搞挂。线程池的诞生就是为了解决这个问题。它本质上是一个“线程复用”的管理器预先创建一批线程任务来了直接交给这些线程处理处理完这个线程不销毁继续处理下一个任务。这样就避免了频繁创建销毁线程的开销同时也限制了并发线程的数量防止资源被耗尽。1.2 线程池能帮我们管理什么从工程角度看线程池的价值可以概括为三点第一复用线程。这是最核心的价值线程的创建成本远高于普通对象的创建复用能大幅减少开销。第二控制并发。通过核心线程数和最大线程数你可以精确控制同时有多少个任务在执行避免线程无限增长拖垮系统。第三提供缓冲。阻塞队列充当了一个“缓冲池”当任务太多、线程处理不过来的时候任务可以暂时排队等待而不是直接丢弃。理解了这三点你再看线程池的各个参数和它的执行流程很多设计就顺理成章了。2. 七个核心参数逐个拆解2.1 ThreadPoolExecutor的构造方法线程池的完整形态就是ThreadPoolExecutor它的一个核心构造方法签名如下public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueueRunnable workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)这就是面试和实践中常说的“线程池七大参数”。很多人记不牢其实可以从“线程怎么来、任务怎么放、满员怎么办”这条逻辑线去理解。七个参数分别是corePoolSize核心线程数常驻线程数量即使空闲也不会被回收除非设置了allowCoreThreadTimeOut。maximumPoolSize最大线程数线程池允许创建的线程上限。keepAliveTime非核心线程的空闲存活时间超过这个时间没有任务处理非核心线程会被回收。unitkeepAliveTime的时间单位。workQueue阻塞队列用于存放等待执行的任务。threadFactory线程工厂用于创建线程可以给线程起名字、设置守护线程等。handler拒绝策略当线程池和队列都满了新任务会被交给这个策略处理。2.2 参数之间不是孤立的理解这些参数不能孤立地一个个去背必须把它们放到执行流程里去看。corePoolSize决定线程池的“基础规模”。当任务到来线程数量还没有达到corePoolSize时线程池会直接创建新线程去处理任务即使已经有空闲线程也一样创建。这一点很多人会搞错甚至一些老手也会忽略。maximumPoolSize是线程池的“极限规模”。当线程数达到corePoolSize之后新任务会先进入阻塞队列排队只有当队列也满了线程池才会继续创建新线程直到达到maximumPoolSize。keepAliveTime则是针对超出corePoolSize部分的线程。如果这些线程空闲了这么久还没有接到新任务就会被回收线程数回落到corePoolSize。实测中keepAliveTime的设置要结合任务到达的间隔来定太短会导致线程频繁创建销毁太长则浪费资源。workQueue是整个流程的“缓冲层”也是最容易被忽视的参数。队列选得不好直接影响线程池的实际行为这一块后面会单独详细讲。threadFactory和handler虽然看起来是配角但在生产环境里它们往往决定你排查问题的效率。自定义threadFactory可以给线程起一个清晰的名字比如order-pool-1一旦线上线程飙高你用jstack一看就知道是哪个业务线程池出了问题。自定义handler则可以把拒绝的任务记录下来而不是静默丢弃。3. 一条任务从提交到执行的完整旅程3.1 核心判断逻辑线程池执行任务核心方法就是execute。我把它的判断逻辑拆成五步这五步就是面试里最常考的“线程池执行流程”。假设你现在调用executor.execute(task)线程池内部会经历以下判断第一步判断当前工作线程数是否小于corePoolSize。如果小于直接调用addWorker创建一个新的工作线程来处理这个任务。注意这里是“小于核心线程数就新建线程”哪怕之前已经有空闲的线程也不会复用。第二步如果当前线程数已经大于等于corePoolSize任务不会直接创建新线程而是尝试把任务放入阻塞队列workQueue也就是调用workQueue.offer(task)。如果入队成功任务就在队列里等待空闲线程来取。第三步入队之后还有一个二次检查。代码会再次判断线程池状态是否还是RUNNING如果不是就把任务从队列中移除并执行拒绝策略。同时判断当前线程数是否为0如果为0会创建一个新的工作线程确保至少有一个线程在处理队列中的任务。第四步如果队列已满入队失败那么线程池会判断当前线程数是否小于maximumPoolSize。如果小于创建新的非核心线程来处理这个任务。第五步如果当前线程数已经等于maximumPoolSize新来的任务无法处理就会执行拒绝策略。3.2 用一个例子走完流程举个具体的例子假设你配置了corePoolSize4、maximumPoolSize8、队列容量100。线程池刚开始运行时工作线程数为0。你连续提交了6个任务前4个任务因为工作线程数从0增长到4每一步都小于corePoolSize所以线程池直接创建4个线程来处理这4个任务。第5个和第6个任务此时工作线程数已经达到corePoolSize4所以它们不会创建新线程而是被放入队列中排队等待空闲线程来处理。这时候如果队列里堆积到了100个任务第101个任务还得排队因为此时你确实没有超过队列容量。现在如果你继续提交3个任务队列已经满了线程池发现当前线程数4小于maximumPoolSize8所以会再创建4个新线程来处理这3个任务加上之前的4个一共8个线程。再往后如果这8个线程都在工作、队列也满了还有新任务过来就触发拒绝策略。这个例子能直观说明一个核心结论队列和线程并不是同时扩大的线程池优先用核心线程其次用队列缓冲最后才扩张到最大线程数。很多人配置了maximumPoolSize100就以为任务一多线程数会直接飙升到100这是不对的。线程数要涨到100前提是队列必须先被填满。3.3 addWorker到底做了什么前面提到了addWorker这是线程池里的核心方法。它做的事情不是简单地new一个Thread而是先检查线程池状态和当前线程数然后通过ReentrantLock加锁在线程数计数上做CAS操作最后才通过ThreadFactory创建线程并启动。这里有几个细节值得注意addWorker里的双重检查是为了应对并发提交。如果两个线程同时提交任务都要创建线程不加锁就会导致线程数超过corePoolSize。线程池在启动线程时不是直接用Runnable作为构造入参那么简单。它会把传入的firstTask封到一个Worker对象里Worker实现了Runnable接口内部又通过AQSAbstractQueuedSynchronizer实现了不可重入的独占锁用来判断工作线程是否空闲。Worker中的runWorker是一个while循环线程从firstTask开始执行执行完之后不停地从阻塞队列里取任务。取不到任务时才会根据超时设置判断是否退出循环并回收线程。这个过程就是线程复用的本质。4. 阻塞队列的选择直接影响线程池行为4.1 常见阻塞队列对比阻塞队列是线程池执行流程中一个至关重要的变量。不同队列的“容量特性和阻塞行为”不同直接决定了任务的排队方式以及线程池什么时候才会扩容到maximumPoolSize。实践中常见的队列有四种我用一个表格把它们的核心特性列出来队列实现是否有界默认容量典型使用场景LinkedBlockingQueue有界默认容量为Integer.MAX_VALUE未指定时近似无界默认配置任务缓冲量大ArrayBlockingQueue有界需要指定容量自定义容量对任务量有明确上限的场景SynchronousQueue不存储元素0直接交接需要立即交给线程处理的场景PriorityBlockingQueue无界无上限需要按优先级处理任务的场景这四种队列中最常踩坑的是LinkedBlockingQueue。如果你用Executors.newFixedThreadPool创建线程池默认使用的就是无界的LinkedBlockingQueue。无界意味着队列永远不会满既然队列不会满线程池自然永远不会创建超过corePoolSize的线程maximumPoolSize这个参数就形同虚设。这是一个非常容易被忽略的设计陷阱。你以为配置了maximumPoolSize20但队列是无界的实际最高并发线程数永远不会超过corePoolSize。在任务量超出核心线程处理能力时所有超出的任务都会堆积在队列里内存消耗会持续增加最终引发OOM。4.2 队列选型的实操建议那到底应该选哪种队列没有统一答案但要依据业务场景来。如果你的任务是短小精悍的CPU密集型任务不希望在队列里等太久可以考虑SynchronousQueue。它不存储任务每个入队操作都必须等待另一个线程执行take操作也就是任务直接交接给工作线程。这种队列配合比较大的maximumPoolSize可以让线程池“一直有新线程来处理任务”而不是让任务排队。如果你的任务量有明确峰值且系统可以接受一定的排队时间那么设置一个有界队列是更稳妥的选择。比如ArrayBlockingQueue容量根据峰值流量以及单任务的平均耗时来估算队列容量 可容忍的排队时间 / 单任务平均耗时。如果你的任务有优先级要求比如重要订单需要先处理那可以选PriorityBlockingQueue。需要注意的是这个队列是无界的一旦任务堆积过多同样有OOM风险使用时要结合熔断、限流等外部手段来控制任务总量。我在实际项目里比较常用的配置是核心线程数4-8有界队列容量几百到一两千maximumPoolSize为核心线程数的2倍拒绝策略选择CallerRunsPolicy或者自定义记录策略。这套组合在正常流量和突发流量下都能稳住。5. execute和submit到底有什么区别5.1 两个方法的定位差异线程池提交任务有两种姿势一种是execute(Runnable command)另一种是submit(Runnable task)或者submit(Callable task)。很多人在代码里混着用看不出问题但其实它们的差异很大。从根本上说execute是ThreadPoolExecutor自己定义的方法只接受Runnable提交后没有返回值任务执行过程中如果抛出异常会直接由线程处理不会返回给调用方。submit则是ExecutorService接口中定义的方法它把Runnable或Callable包装成一个FutureTask提交后返回一个Future对象。调用方可以通过Future.get()获取任务执行结果也可以用它来感知任务是否执行完毕。从执行流程来看submit底层依旧调用execute但它传入的参数不是原始任务而是一个FutureTask。FutureTask内部封装了原始任务并且在任务执行结束后会把结果或异常保存在内部字段里等待Future.get()来取。5.2 异常处理的差别这里有一个非常关键的区别就是异常处理。使用execute提交任务如果任务内部抛出了未捕获的RuntimeException异常会直接传导到线程池的工作线程。在线程池的runWorker方法中所有抛出的异常会让当前线程退出线程池会创建一个新线程来补充。也就是说异常本身不会静默消失但你也无法通过调用方捕获并处理。使用submit提交任务情况就完全不一样了。如果你提交的任务是Runnable并且在任务内抛出了异常这个异常会被捕获并保存到FutureTask内部。除非你调用future.get()否则异常完全不会暴露出来甚至不会有任何日志。这就是业界常说的“submit的任务异常会被吞掉”的坑。我给大家一个建议如果你的业务逻辑需要感知任务执行失败并且要针对性处理建议不要直接submit(Runnable)而是使用submit(Callable)或者在自己的Runnable内部显式捕获异常并记录日志。不然线上出了问题你翻半天日志都找不到原因。如果你真的需要在任务执行完之后获取结果并使用可以用以下方式ExecutorService pool Executors.newFixedThreadPool(4); FutureResult future pool.submit(() - processOrder(orderId)); try { Result result future.get(5, TimeUnit.SECONDS); } catch (TimeoutException e) { // 处理超时 } catch (ExecutionException e) { // 处理任务内部抛出的异常e.getCause()是真正的异常 }这里有一个容易被忽略的点future.get()是有阻塞特性的如果没有设置超时时间调用方可能会被一直阻塞到任务完成。所以在生产代码里务必使用带超时参数的get(long timeout, TimeUnit unit)否则一旦任务卡死调用线程也会被拖死。5.3 用Future优雅任务编排如果你使用submit并且拿到了Future其实还能做一些轻量级的任务编排。比如你先提交三个并行且互不依赖的任务然后调用三个Future的get等待所有任务完成再汇总结果。这种模式虽然没有CompletableFuture那么优雅但在简单场景下已经够用。不过要记住Future的get是阻塞的提交多个任务后如果按顺序逐个get就相当于把并行又变回串行。正确做法是先把所有任务都submit拿到全部Future后再统一get这样才能真正并行执行。6. 工作线程的运转与回收机制6.1 工作线程的启动线程池中的工作线程并不是在你调用execute的那一刻就直接全部创建的。前面提到当任务数小于corePoolSize时任务来一个创建一个线程。实际上线程池并不会预先把corePoolSize个线程全部创建好它倾向于“懒创建”也就是有任务了才创建除非调用了prestartAllCoreThreads方法。这个特性在日常使用中问题不大因为在稳态下经过一段时间的运行线程数会稳定在corePoolSize附近。但是如果你对系统的预热时间有要求比如希望线程池在流量洪峰到来之前就已经准备好核心线程那可以在应用启动后主动调用prestartAllCoreThreads。6.2 线程的循环取任务机制工作线程启动后它的任务来源有两个一个是创建时传入的firstTask另一个是阻塞队列workQueue里的任务。runWorker的核心逻辑可以简化为while (task ! null || (task getTask()) ! null) { process(task); task null; }第一次循环task就是firstTask执行完之后task被置为null。第二次循环调用getTask()从阻塞队列里取一个新的任务。如果队列为空getTask会阻塞等待等待时间由keepAliveTime控制。这里就是线程复用的精妙之处线程不是执行完一个任务就结束而是回到循环里继续从队列取任务直到getTask返回null。6.3 非核心线程的回收getTask在什么情况下会返回null呢一种情况是线程池被shutdown另一种就是当前线程数大于corePoolSize并且线程在keepAliveTime时间内都没有取到任务。具体来说当线程数大于corePoolSize时getTask在调用workQueue.poll(keepAliveTime, unit)时如果超过keepAliveTime还没有取到任务就会返回null然后当前工作线程退出完成回收。如果线程数小于等于corePoolSizegetTask执行的是workQueue.take()这是一个会无限期阻塞等待的操作线程会一直在那等着除非线程池关闭。所以核心线程是非到万不得已不会被回收的。如果你希望核心线程也能在一段时间空闲后被回收可以开启allowCoreThreadTimeOut(true)这样核心线程也可以被回收线程数甚至可以降到0。这个开关适合那种任务量极不均衡、大部分时间处于空闲状态的定时任务场景。7. 拒绝策略的四种玩法7.1 内置四种拒绝策略当线程池已经达到maximumPoolSize队列也已经满了此时还有新任务提交就会触发RejectedExecutionHandler。ThreadPoolExecutor内置了四种拒绝策略策略行为使用场景AbortPolicy直接抛出RejectedExecutionException默认策略适合要明确定位问题的场景CallerRunsPolicy在调用方线程执行任务适合不想丢任务、可以牺牲响应时间的场景DiscardPolicy静默丢弃任务适合允许丢失非重要任务的场景DiscardOldestPolicy丢弃队列中最旧的任务然后重新提交适合希望丢弃旧任务、保留新任务的场景7.2 默认策略为什么不适合生产如果你不额外指定拒绝策略默认是AbortPolicy也就是直接抛RejectedExecutionException。在笔者的经验里这个默认策略在测试环境还能接受因为异常会立刻暴露问题但在生产环境直接抛异常往往意味着前端用户看到的是500错误而且线程池已经被打满了再抛异常也不过是雪上加霜。更麻烦的是AbortPolicy抛出的异常如果没被上层捕获只会打印在异常日志里Task到底是谁提交的、为什么会提交这么多没有任何上下文。排查起来效率很低。7.3 自定义拒绝策略才是王道我的建议是生产环境必须自定义拒绝策略核心目标就两条第一保留现场第二尽量补偿处理。比如我在一个订单推送服务里自定义的拒绝策略是这样做的先把被拒绝的任务序列化成JSON写入一个独立的日志文件记录任务内容、当前线程池状态、队列长度、提交时间。然后将任务信息发送到告警系统人工收到通知后可以介入排查。如果该任务允许延迟塞到一个“重试队列”里由定时任务在低峰期重新提交。这一步非常实用。你想想用户辛辛苦苦下的订单推送通知如果被静默丢弃用户可能永远收不到发货消息。有了重试机制即使线程池打满核心业务也不会丢。需要特别指出的是自定义拒绝策略里的操作不能太重因为拒绝策略是在调用方线程里执行的如果这里做了太重的IO操作反而会拖慢调用方的响应时间。8. 常见问题与排查技巧实录8.1 线程池中的线程数涨不上去这是一个非常经典的问题。明明配置了maximumPoolSize20但线上观察线程数一直停在corePoolSize再也没有往上走。先不要怀疑配置没生效绝大多数情况是队列容量设置太大或者队列是无界的。因为线程池的判断逻辑是核心线程满了先入队队列满了才扩容。如果你的队列容量是10000而实际任务最多只积累到几百线程池永远不会触发扩容逻辑。排查方法也很简单查看线程池的activeCount和queue.size如果queue.size一直大于0但没有增长到队列上限说明线程数涨不上去是因为队列容量富余。此时如果你希望更快扩容可以调小队列容量或者换用SynchronousQueue。另外还有一种可能是核心线程数配置过高。比如corePoolSize20maximumPoolSize40那线程数涨到20就满足了“核心线程”阶段队列不满的话当然不会再涨到40。所以配置参数的时候建议把corePoolSize和maximumPoolSize拉开明显差距否则容易误解。8.2 线程数飙升甚至打满了CPU和线程数涨不上去相反有些场景下线程数一路暴涨直接打到maximumPoolSizeCPU也跟着飙升。这种情况通常是因为任务本身执行时间过长或者任务内部有阻塞操作。最典型的例子是把IO操作直接丢到线程池里执行比如在任务里调用远程接口、执行数据库查询、或者等待锁。这些阻塞操作本身不消耗CPU但会让线程一直占着不释放于是线程数持续上升。解法有两个方向第一针对IO密集型的任务单独设置一个线程池参数根据IO等待比例来调第二对任务执行时间设置超时机制超时任务要能中断不能让一个死循环或长时间阻塞的任务永久占着线程。一个经验公式如果任务是CPU密集型的核心线程数接近CPU核数加1即可如果任务有IO等待可以适当调大比如CPU核数乘以2或者根据耗时占比计算。8.3 任务积压导致OOM前面提过无界队列的潜在风险这里再具体说一种OOM场景。假设你使用Executors.newFixedThreadPool(4)创建线程池它内部的LinkedBlockingQueue默认容量是Integer.MAX_VALUE。如果某个时刻上游任务突然爆发比如营销活动瞬间产生大量异步请求这些任务会源源不断进入队列排队。由于队列没有上限内存中不断积累Runnable对象最终导致堆内存溢出。不要觉得这只是极端情况。我在一次大促前做过压测活动触发任务的QPS到了几千而消费线程的核心线程数只有8个队列在几十秒内就积累了几十万个任务堆内存直接顶不住。所以所有线程池的队列都必须显式设置容量尤其是异步任务多的系统宁可配合拒绝策略丢一些不重要的任务也好过整台机器挂掉。8.4 线程池关闭时任务丢失另一个容易踩坑的点是线程池的关闭时机。如果在任务还没执行完的时候调用了shutdownNow线程池会中断所有正在执行的任务并返回尚未开始执行的任务列表。这些任务不会自动被处理需要你自己决定如何补偿。shutdown和shutdownNow的区别很简单shutdown会等待队列中的任务执行完毕后关闭不会再接收新任务shutdownNow会立刻中断所有任务并尝试停止正在执行的任务。在实际业务中我建议在应用优雅停机时先调用shutdown然后等待一定的宽限期比如30秒如果还没有完全关闭再调用shutdownNow。这样既给了正在执行的任务一个收尾的机会也避免停机时间过长。8.5 线程池监控数据怎么拿聊完这些常见问题最后再提供一个排查线程池问题非常好用的技巧给线程池加上监控。ThreadPoolExecutor自带了一些可以用于监控的方法比如getPoolSize获取当前线程数getActiveCount获取活跃线程数getQueue().size()获取队列长度getCompletedTaskCount获取已完成任务数getTaskCount获取总任务数。可以把这些指标定期采集到监控系统绘制曲线观察线程池的负载变化。我在生产环境里就是每隔30秒采样一次线程池的核心指标并打印一条日志order-pool core8 max16 current14 active12 queue260 completed10239这样哪怕没有接入完整的监控系统也可以从日志里粗粒度地判断线程池是否健康。如果thread count持续保持在maxqueue持续增长说明线程池已经超负荷如果queue一直为空active很低说明线程池的容量还有富余。自定义ThreadFactory之后线程池的线程名也会出现在日志和jstack里排查时能一眼看到是哪个业务池子出了问题这个习惯真的能在关键时刻救命。有条件的团队还可以把监控做成定期快照记录每一次拒绝策略触发时的上下文信息方便事后复盘。9. 线程池配置的实战经验9.1 参数配置没有银弹很多人问线程池参数到底该怎么配我想强调一个观点线程池配置没有统一公式必须结合业务场景来定。如果你的任务是CPU密集型核心线程数可以设为CPU核心数加1因为这种任务几乎一直占用CPU过多的线程反而会导致上下文切换开销增大。如果你的任务是IO密集型比如远程调用、读写文件、访问数据库线程的CPU占用率低大部分时间都在等待IO。此时可以适当提高线程数比如CPU核心数乘以2或者根据IO等待时间的比例来计算。经验公式是线程数 CPU核心数 * (1 等待时间 / 计算时间)。如果你的任务类型混杂那就需要按任务类型拆分成多个线程池避免一种任务拖累另一种。9.2 动态调整线程池参数还有一个进阶玩法利用ThreadPoolExecutor提供的setCorePoolSize、setMaximumPoolSize方法在运行时动态调整线程池参数。一套比较稳妥的做法是线程池初期的参数不要配置太大先以测试环境的数据为准。等系统上线后根据监控数据逐步调整。如果发现线程池长期空闲可以下调核心线程数如果发现队列经常积压可以适当上调最大线程数或者增加核心线程数。这样做的本质是根据真实流量做反馈式调节比拍脑袋定参数靠谱得多。9.3 不要用Executors的快捷方法最后关于线程池配置我还要给一个非常明确、非常坚决的建议不要使用Executors提供的newFixedThreadPool、newCachedThreadPool、newScheduledThreadPool这些快捷方法。原因很简单newFixedThreadPool和newSingleThreadExecutor的队列是无界的会OOMnewCachedThreadPool的最大线程数是Integer.MAX_VALUE极端情况下会创建过多线程导致OOM或者线程栈溢出。Java开发规范里也明确不推荐使用这些快捷方法。规范的做法是自己通过ThreadPoolExecutor构造方法创建线程池并显式指定所有参数。尽管代码看起来更长但每一步都是可控的出问题时也能更快定位。我之前处理过一个线上故障某个服务使用了Executors.newCachedThreadPool接收异步任务并发一高线程数直接冲到了几千最后把CPU占满了。排查了半天发现就是快捷方法埋的雷。这里想多说一句很多人觉得newCachedThreadPool“线程不够就创建空闲就回收”听起来很智能但问题恰恰出在“线程不够就创建”上面这种模式下线程数几乎是不设上限的。配合SynchronousQueue任何一个提交的任务都会立刻创建一个线程去处理如果任务提交速度快于线程处理速度线程数会无限膨胀直到内存耗尽。所以手动配置线程池确实麻烦但这个麻烦是值得的毕竟它换来的是可预期、可控制、可排查。10. 从源码角度再看一眼关键实现10.1 execute方法的伪代码视角为了把执行流程再钉死一遍我习惯把execute方法高度抽象成下面几行逻辑public void execute(Runnable command) { if (command null) throw new NullPointerException(); int c ctl.get(); if (workerCountOf(c) corePoolSize) { if (addWorker(command, true)) return; c ctl.get(); } if (isRunning(c) workQueue.offer(command)) { int recheck ctl.get(); if (!isRunning(recheck) remove(command)) reject(command); else if (workerCountOf(recheck) 0) addWorker(null, false); } else if (!addWorker(command, false)) { reject(command); } }这段代码就是整个执行流程的浓缩。如果能把这段代码里的每个分支都讲清楚那你对线程池的执行流程就真正掌握了。简单翻译一下这段逻辑如果工作线程数小于核心线程数就新建线程否则如果线程池在运行且入队成功再过一遍状态和线程数如果入队失败尝试创建非核心线程失败就执行拒绝策略。这里的ctl是一个AtomicInteger里面同时包装了线程池的运行状态和工作线程数高3位表示运行状态低29位表示线程数。这种设计是为了在一个CAS操作里同步修改两个变量避免锁竞争。对源码细节感兴趣的朋友可以拿着源码一行一行读runWorker和getTask这两个方法是理解线程复用的关键所在。10.2 状态流转线程池内部有几种状态虽然不直接参与执行流程但理解了它们能帮你更清楚地把握线程池的生命周期。RUNNING接收新任务处理队列任务。SHUTDOWN不再接收新任务但处理已入队任务。STOP不接收新任务不处理队列任务中断正在执行的任务。TIDYING所有任务已终止工作线程数为0即将执行terminated钩子方法。TERMINATEDterminated方法执行完成。这里有一个常见的面试追问场景正在执行的任务如果被shutdownNow会不会被中断答案是取决于任务本身是否响应中断。如果任务内部没有处理中断信号shutdownNow也无法强制停止它。这提醒我们在编写长时间运行的任务时要主动检查Thread.currentThread().isInterrupted()以便在外部关闭线程池时能够及时退出。11. 关于线程池执行流程的几个关键心得讲了这么多最后再分享几条我个人在实际项目里的体会。线程池是并发编程里一个典型的“看起来简单、用起来容易翻车”的组件。它之所以复杂不是因为它本身有多高深而是因为参数之间的联动关系太多一个参数变了整个行为就会完全不同。建议所有团队在引入线程池之前先明确几个问题任务允许排队吗允许等多久任务积压时是丢还是重试线程池整体占用的资源上限是多少这几个问题想明白了参数配置就顺理成章了。另外哪怕你的线程池参数配置得再合理也一定要配合监控一起上线。没有任何监控的线程池就像蒙着眼睛开车出了问题往往已经晚了。哪怕只是打印一条周期性的日志也比完全黑盒要强得多。最后再提一个小技巧假设你用的是Spring可以通过ThreadPoolTaskExecutor来包装线程池并且设置ThreadPoolExecutor.setWaitForTasksToCompleteOnShutdown(true)和setAwaitTerminationSeconds(30)这样在应用关闭时能够优雅地等待正在执行的任务完成而不是被强制中断。我之前在一个项目里就吃过亏应用重启的时候旧线程池里的任务还在写数据库新进程已经开始启动了导致数据被重复写或者丢失。后来加上优雅停机配置这个问题就再也没出现过。线程池的执行流程本质上是一个“先自己干干不过来排队队满了临时加人加完人还不行就拒绝”的模型。把这句话理解透了线程池这条主线就能立得住。剩下的所有细节都是在为这个模型服务。
