
文章目录为什么需要线程池核心字段 ctl一个 int 存两件事提交一个任务后发生了什么Worker线程池里的工人七个构造参数阻塞队列怎么选拒绝策略默认那个最刺眼一个必须知道的坑无界队列让 maximumPoolSize 失效提交任务execute 还是 submit关闭线程池shutdown 与 shutdownNow线程数到底开多少为什么不推荐 Executors 快捷工厂线程池的监控小结多线程是面试高频区也是线上事故高频区。线程池用错了轻则任务堆积、接口超时重则 OOM、整个服务被打挂。这篇文章从ThreadPoolExecutor.execute()的源码出发把「任务提交后到底发生了什么」讲清楚再落到参数配置、拒绝策略、关闭方式和监控。为什么需要线程池直接new Thread()的问题很明确创建/销毁开销大每次创建线程都要向 OS 申请栈空间、注册到调度器销毁又要回收。任务本身可能只跑几毫秒线程创建的成本反而占大头。无法控制数量请求量一上来就是无限创建线程数暴涨CPU 在上下文切换上耗尽直接把机器拖死。缺少统一管理没有队列缓冲、没有拒绝策略、没有统计信息出了问题只能靠猜。线程池解决的正是这三点复用线程降低资源消耗、任务排队避免瞬时打满、统一管控线程数量并提供监控指标。核心字段 ctl一个 int 存两件事看源码之前必须先认识ctl它是整个类的中枢privatefinalAtomicIntegerctlnewAtomicInteger(ctlOf(RUNNING,0));privatestaticfinalintCOUNT_BITSInteger.SIZE-3;// 29privatestaticfinalintCAPACITY(1COUNT_BITS)-1;// 低 29 位ctl是一个 32 位整数被切成两段位段含义高 3 位runStateRUNNING / SHUTDOWN / STOP / TIDYING / TERMINATED低 29 位workerCount当前有效线程数上限约 5 亿这么设计的目的只有一个状态和线程数用一次 CAS 原子更新避免两个字段之间的竞态。当你要同时判断「池是否在运行」和「线程数是否够」时单次读ctl就能拿到一致的快照。提交一个任务后发生了什么当向线程池提交一个任务之后线程池是如何处理这个任务的呢先看整体流程execute()的逻辑可以概括成四步判断按顺序短路步骤判断条件动作1workerCount corePoolSizeaddWorker(command, true)建核心线程成功即返回2核心已满workQueue.offer()成功任务入队再复查池状态与 worker 数3入队失败队列满addWorker(command, false)扩容到maximumPoolSize4扩容也失败reject(command)交给拒绝策略四条路径画出来是这样图里的 1~4 对应表格里的步骤源码JDK 8去掉注释publicvoidexecute(Runnablecommand){if(commandnull)thrownewNullPointerException();intcctl.get();// 1. 核心线程未满直接新建线程执行if(workerCountOf(c)corePoolSize){if(addWorker(command,true))return;cctl.get();}// 2. 核心已满尝试入队if(isRunning(c)workQueue.offer(command)){intrecheckctl.get();// 入队后池被关闭回滚把任务从队列里摘出来再拒绝if(!isRunning(recheck)remove(command))reject(command);// 池还在运行但没有 worker 了核心线程允许超时回收时会遇到补一个elseif(workerCountOf(recheck)0)addWorker(null,false);}// 3. 入队失败扩容非核心线程仍失败则拒绝elseif(!addWorker(command,false))reject(command);}两个容易被忽略的细节第一第 2 步里offer成功后必须recheck。因为这个间隙里线程池可能已经被shutdown此时任务已经躺在队列里却永远不会被执行必须remove出来走拒绝流程。这是并发代码里典型的 “check-then-act” 修正。第二addWorker内部要抢mainLock全局锁。也就是说只有建线程这条路径是串行的任务入队本身走的是并发队列不影响吞吐。反过来说如果队列很小、频繁触发扩容建线程锁竞争就会成为瓶颈——这也是为什么不要配一个极小的队列。Worker线程池里的工人线程池没有直接用Thread而是包了一层WorkerprivatefinalclassWorkerextendsAbstractQueuedSynchronizerimplementsRunnable{finalThreadthread;// Worker 自己持有一个线程RunnablefirstTask;// 首个任务可以为 nullvolatilelongcompletedTasks;}Worker继承 AQS 是为了用一个不可重入的锁标记「这个工人是否正在执行任务」——shutdown时靠它区分空闲线程和忙碌线程只中断空闲的。真正的执行循环在runWorker()finalvoidrunWorker(Workerw){Runnabletaskw.firstTask;w.firstTasknull;while(task!null||(taskgetTask())!null){// ... 加锁、beforeExecute、task.run()、afterExecutetasknull;}}关键在getTask()工人执行完一个任务不会退出而是回头向队列要下一个。privateRunnablegetTask(){booleantimedallowCoreThreadTimeOut||wccorePoolSize;// timed 为 true 用 poll(keepAliveTime)超时返回 null → 线程退出// timed 为 false 用 take()队列空就一直阻塞Runnablertimed?workQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS):workQueue.take();if(r!null)returnr;// ...}工人与队列的交互画出来是这样——execute()把任务推进队列工人线程通过take()/poll()从队列里拉任务getTask()返回null是工人退出的唯一信号。所以keepAliveTime的真实含义是非核心线程或开启了allowCoreThreadTimeOut的核心线程在队列上等待多久拿不到任务就自杀。七个构造参数publicThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,BlockingQueueRunnableworkQueue,ThreadFactorythreadFactory,RejectedExecutionHandlerhandler)参数含义实践要点corePoolSize核心线程数常驻工人数默认不会被回收maximumPoolSize最大线程数队列满了才扩容到这里keepAliveTime空闲存活时间只对超出核心数的线程生效unit时间单位TimeUnit.SECONDS最常用workQueue任务队列最容易配错的一个见下节threadFactory线程工厂一定要给线程起名字排查问题时能救命handler拒绝策略默认AbortPolicy会抛异常线程工厂别偷懒ThreadFactoryfactorynewThreadFactoryBuilder().setNameFormat(order-pool-%d).setDaemon(false).build();线上 dump 线程栈时pool-1-thread-3和order-pool-3的排查效率差一个数量级。阻塞队列怎么选队列结构是否有界特点ArrayBlockingQueue数组有界必须指定容量FIFO可控性最好LinkedBlockingQueue链表默认Integer.MAX_VALUE事实上无界吞吐高于数组版两个锁分离SynchronousQueue不存储元素容量 0入队必须有人同时在取否则失败PriorityBlockingQueue堆无界按Comparable排序不保证同优先级 FIFODelayQueue堆无界到期才能取定时任务用SynchronousQueue的行为值得单独说offer只有在恰好有工人正在take时才成功否则立即返回false。所以配SynchronousQueue的池几乎总是走第 3、4 步——直接扩容建线程扩容不了就拒绝。newCachedThreadPool就是这套配置。拒绝策略默认那个最刺眼策略行为风险AbortPolicy默认抛RejectedExecutionException调用方没 catch 就是一次失败CallerRunsPolicy由提交任务的线程自己执行反压回上游能天然限流但会阻塞调用线程DiscardPolicy静默丢弃最危险任务凭空消失没有任何日志DiscardOldestPolicy丢掉队列头最老的任务再重试提交丢的是最重要的老任务慎用生产环境里DiscardPolicy基本一票否决。要么用CallerRunsPolicy做反压要么自定义一个「记录日志 打点 抛异常」的策略把拒绝这件事暴露出来。一个必须知道的坑无界队列让 maximumPoolSize 失效回想 execute 的四步只有入队失败才会去扩非核心线程。而LinkedBlockingQueue默认容量是Integer.MAX_VALUE几乎不可能入队失败。结果就是corePoolSize10, maximumPoolSize200配上无界的LinkedBlockingQueue线程数永远停在 10剩下的任务全在队列里排队maximumPoolSize和keepAliveTime形同虚设。任务越积越多最后 OOM。记住一句话队列容量决定了线程池的缓冲还是扩容性格。想让它扛突发并发就必须用有界队列。提交任务execute 还是 submit两种提交方式对应两类任务IDE 里能看到ThreadPoolExecutor一共有三个submit重载方法入参返回异常处理execute(Runnable)Runnablevoid未捕获异常直接抛到afterExecute/ 线程的UncaughtExceptionHandlersubmit(...)Runnable/CallableTFutureT异常被吞进 Future不get()永远看不到Callable相比Runnable多了返回值和受检异常的能力。Future.get()拿结果get(timeout, unit)带超时isDone()判完成。这是线上最常见的坑之一用submit提交任务任务内部抛了异常没有任何日志——因为异常被封装进Future了。如果代码从不调用get()异常就彻底消失。要么改用execute要么在任务体内自己 try-catch要么确保get()被调用。另外get()是阻塞的会一直等到任务结束不要在 IO 线程或主流程里无超时地get()。关闭线程池shutdown 与 shutdownNow维度shutdown()shutdownNow()最终状态SHUTDOWNSTOP新任务拒绝拒绝队列中已提交的任务继续执行完取出并返回不执行正在执行的任务不打断尝试interrupt返回值无ListRunnable未执行的任务两者都会调用interruptIdleWorkers()中断空闲线程——注意是空闲的shutdown不会打断正在跑的任务靠的还是getTask()返回null让工人自然退出。两个状态判断方法经常被搞混isShutdown()只要调用了shutdown或shutdownNow就返回true不代表任务执行完了。isTerminated()所有任务执行完、池真正终止才返回true。通常配合awaitTermination(timeout, unit)使用。优雅关闭的标准写法pool.shutdown();try{if(!pool.awaitTermination(30,TimeUnit.SECONDS)){pool.shutdownNow();// 超时强杀if(!pool.awaitTermination(10,TimeUnit.SECONDS))log.warn(线程池未能正常终止);}}catch(InterruptedExceptione){pool.shutdownNow();Thread.currentThread().interrupt();// 恢复中断标记}最后那行interrupt()很容易漏。捕获InterruptedException会清掉中断标记不恢复的话上层逻辑就感知不到中断了。线程数到底开多少先分清任务类型任务类型特征经验公式CPU 密集型大量计算、少阻塞N 1N CPU 核数IO 密集型大量等待网络/磁盘2N或N × (1 等待时间/计算时间)混合型两者都有拆成两个池分别处理别混在一个池里N 1的理由CPU 密集时线程几乎不阻塞多一个线程用来在偶发缺页时顶上再多就是纯上下文切换开销。N × (1 WT/ST)才是 IO 密集的理论依据——WT是等待时间ST是计算时间。等得越久能并行的线程越多。如果数据库 RT 是 50ms、计算只要 5ms那N × 11都不算多。但公式只是起点。真正的答案是压测出来的公式只用来给一个初始值别当结论用。另外两条硬约束优先用有界队列别让任务无限堆积。不同业务用不同线程池别让一个慢接口把整个池的线程全占住线程池隔离的意义就在这。为什么不推荐 Executors 快捷工厂工厂方法核心/最大队列隐患newFixedThreadPooln / nLinkedBlockingQueue无界任务堆积 → OOMnewSingleThreadExecutor1 / 1LinkedBlockingQueue无界同上newCachedThreadPool0 /Integer.MAX_VALUESynchronousQueue线程数无上限 → 打爆机器newScheduledThreadPooln /Integer.MAX_VALUEDelayedWorkQueue无界任务堆积前两个是队列无界后两个是线程无界都不是安全默认值。阿里 Java 开发手册直接禁止用Executors创建线程池要求手动new ThreadPoolExecutor原因就在这里——你必须自己想清楚容量边界。线程池的监控线程池提供了三个钩子继承ThreadPoolExecutor重写即可publicclassMonitorablePoolextendsThreadPoolExecutor{OverrideprotectedvoidbeforeExecute(Threadt,Runnabler){super.beforeExecute(t,r);}OverrideprotectedvoidafterExecute(Runnabler,Throwablet){// t 不为 null 说明任务抛异常了这里是最后的捕获点if(t!null)log.error(task failed,t);super.afterExecute(r,t);}Overrideprotectedvoidterminated(){super.terminated();}}常用的状态查询 API方法含义getActiveCount()正在执行任务的线程数近似值getPoolSize()当前线程总数getQueue().size()排队任务数最该告警的指标getCompletedTaskCount()累计完成任务数getLargestPoolSize()历史峰值线程数队列长度是最灵敏的信号它开始持续增长说明消费能力已经跟不上生产速度再过一会儿就是拒绝或 OOM。建议对queue.size()和activeCount/maximumPoolSize都设阈值告警。小结execute的四步判断决定了整个池的行为队列容量是其中的命门。maximumPoolSize只在队列满时才生效无界队列直接废掉它。拒绝策略默认抛异常DiscardPolicy等于静默丢数据。submit的异常藏在Future里不get()就等于没有。shutdown等任务跑完shutdownNow抢任务列表isShutdown≠isTerminated。线程数公式只是初始值压测才是答案不同业务拆不同池。别用Executors手动构造并给线程起有意义的名字。监控queue.size()它是线程池最早发出的求救信号。