Java 线程池别再背参数了:7 个参数、4 种拒绝策略,三个实测实验看清执行顺序
发布时间:2026/10/15 15:00:11 锦皓数字建站

文章目录一、七个参数先解决「谁决定什么」1.1 提交任务时的真实顺序这段是全文骨架1.2 线程工厂一定要自己传二、实验一同一批任务只换拒绝策略三、实验二submit 的异常是静悄悄的四、四种拒绝策略对照与选型五、核心线程数怎么定两个公式 一个必做的动作六、还有三个容易忽略的点七、小结面试问线程池多数人能背出七个参数的名字但问到「核心线程满了之后先排队还是先加线程」「maximumPoolSize什么时候是摆设」「submit里抛异常为什么静悄悄」就开始含糊了。这篇不背概念用三个可运行的小实验把执行顺序和拒绝策略跑给你看同一个池子、同一批任务只换队列或拒绝策略输出完全不同。文末给出参数估算公式和五个线上必踩的坑。实测环境JDK 21ThreadPoolExecutor的行为在 JDK 8 到 21 之间没有语义变化JDK 8 同样适用。一、七个参数先解决「谁决定什么」publicThreadPoolExecutor(intcorePoolSize,// 1. 核心线程数intmaximumPoolSize,// 2. 最大线程数longkeepAliveTime,// 3. 非核心线程空闲存活时间TimeUnitunit,// 4. 上面的时间单位BlockingQueueRunnableworkQueue,// 5. 任务队列ThreadFactorythreadFactory,// 6. 线程工厂起名RejectedExecutionHandlerhandler)// 7. 拒绝策略七个数里真正需要你反复权衡的只有三个核心线程数、队列类型与容量、拒绝策略。1.1 提交任务时的真实顺序这段是全文骨架execute(task) │ ├─ 1. 线程数 corePoolSize → 直接新建核心线程执行 │ ├─ 2. 核心线程都忙 → 尝试放入 workQueue │ 放入成功 → 排队等空闲线程来取 │ ├─ 3. 队列满了 → 线程数 maximumPoolSize │ → 新建非核心线程执行 │ └─ 4. 队列满 线程数 maximumPoolSize → 交给拒绝策略关键点先排队后扩容。很多人下意识以为「任务多了就加线程」实际是「核心线程满了先塞队列队列满了才加线程」。这个顺序决定了队列类型的重要性——下面实验三会看到队列选错maximumPoolSize直接变成摆设。1.2 线程工厂一定要自己传ThreadFactoryfactorynewThreadFactory(){privatefinalAtomicIntegernnewAtomicInteger(1);OverridepublicThreadnewThread(Runnabler){ThreadtnewThread(r,order-pool-n.getAndIncrement());t.setDaemon(false);returnt;}};默认工厂起的线程叫pool-1-thread-1线上jstack一导几十个池子的线程混在一起分不清是哪个业务的。给池子起名是排查问题的第一笔投资成本一行代码。二、实验一同一批任务只换拒绝策略实验参数刻意设小方便一眼看出排队和拒绝核心 2、最大 4、ArrayBlockingQueue容量 2、一次性提交 8 个任务。算一下就知道必然有任务被拒2 个核心线程 2 个队列位 2 个扩容线程 6 个能容纳第 7、8 个触顶。staticvoidrun(Stringtitle,BlockingQueueRunnablequeue,RejectedExecutionHandlerhandler,inttaskCount)throwsException{ThreadPoolExecutorpoolnewThreadPoolExecutor(2,4,60,TimeUnit.SECONDS,queue,newThreadFactory(){privateintn0;publicThreadnewThread(Runnabler){returnnewThread(r,worker-(n));}},handler);for(inti1;itaskCount;i){try{pool.execute(newTask(i));}catch(RejectedExecutionExceptione){System.out.println( 任务i 被拒绝抛 RejectedExecutionException);}}pool.shutdown();pool.awaitTermination(10,TimeUnit.SECONDS);System.out.println( 峰值线程数pool.getLargestPoolSize(), 完成任务数pool.getCompletedTaskCount());}A 组AbortPolicy默认本机实际输出 A. 核心2/最大4/有界队列(cap2) AbortPolicy提交 8 个任务 任务7 被拒绝抛 RejectedExecutionException 任务8 被拒绝抛 RejectedExecutionException 任务1 由 [worker-1] 执行 任务6 由 [worker-4] 执行 任务5 由 [worker-3] 执行 任务2 由 [worker-2] 执行 任务3 由 [worker-3] 执行 任务4 由 [worker-1] 执行 峰值线程数4, 完成任务数6任务 7、8 被拒。注意打印顺序拒绝信息先出现因为execute是同步判断的而任务的实际执行在子线程里输出顺序随机。这说明一件事——拒绝决定了任务连排队资格都没有是唯一的硬性丢失点。B 组只换CallerRunsPolicy B. 同上参数 CallerRunsPolicy提交 8 个任务 任务7 由 [main] 执行 任务8 由 [main] 执行 任务1 由 [worker-1] 执行 ... 峰值线程数4, 完成任务数6任务 7、8 由main线程执行了——CallerRunsPolicy让提交任务的线程自己去跑。这个策略的价值不是「多干点活」而是天然的背压提交方被拖住执行就没法继续往池子里灌任务上游速度自动降下来。对不能丢任务的场景数据同步、可靠消息消费这是最合适的选择。C 组无界队列这是最值得看的一组 C. 核心2/最大4/无界队列 AbortPolicy提交 8 个任务看峰值线程数 任务1 由 [worker-1] 执行 任务2 由 [worker-2] 执行 任务4 由 [worker-1] 执行 任务3 由 [worker-2] 执行 任务6 由 [worker-1] 执行 任务5 由 [worker-2] 执行 任务7 由 [worker-1] 执行 任务8 由 [worker-2] 执行 峰值线程数2, 完成任务数8任务全部完成了但峰值线程数只有 2——maximumPoolSize4一个线程都没多起。原因就是第 1.1 节的顺序无界队列永远装得下 → 永远进不到「扩容」那一步 →maximumPoolSize形同虚设。这就是阿里开发规约里禁止Executors.newFixedThreadPool/newSingleThreadExecutor的根本原因——它们用的都是无界队列任务堆积只会在队列里涨最后以 OOM 收场而不是干脆地抛拒绝。同样的道理Executors.newCachedThreadPool另一个极端maximumPoolSize Integer.MAX_VALUE任务一多线程数无上限直接打爆操作系统线程数。结论一句话自己new ThreadPoolExecutor用有界队列显式写清七个数。三、实验二submit的异常是静悄悄的同一个池子execute和submit执行同样的抛异常任务本机实测System.out.println(-- 执行 execute(Runnable)任务里抛异常 --);pool.execute(()-{thrownewRuntimeException(execute 的任务炸了);});Thread.sleep(300);System.out.println(-- 执行 submit(Runnable)任务里抛同样的异常 --);pool.submit(()-{thrownewRuntimeException(submit 的任务炸了);});Thread.sleep(300);System.out.println(-- submit Future.get() 才能看到异常 --);Future?fpool.submit(()-{thrownewRuntimeException(这次被 get 出来了);});try{f.get();}catch(ExecutionExceptione){System.out.println( get() 抛出 ExecutionException原因是: e.getCause().getMessage());}真实输出-- 执行 execute(Runnable)任务里抛异常 -- Exception in thread pool-1-thread-1 java.lang.RuntimeException: execute 的任务炸了 at SubmitDemo.lambda$main$0(SubmitDemo.java:8) ... -- 执行 submit(Runnable)任务里抛同样的异常 -- -- submit Future.get() 才能看到异常 -- get() 抛出 ExecutionException原因是: 这次被 get 出来了 -- 主线程正常结束 --中间那三行输出后面什么都没有——submit把异常封进了Future你不调get()就永远看不到。这是线上最难查的一类问题任务明明跑了业务数据没写日志一片干净。两种解法用submit就必须get()或者在CompletableFuture链上加exceptionally/whenComplete更省事的办法——在任务体里自己 try/catch别指望池子替你兜。任务最外层包一层 try-catch 打印日志并上报是生产代码的标配。四、四种拒绝策略对照与选型策略行为适合场景风险AbortPolicy默认抛RejectedExecutionException需要严格感知过载、能接受快速失败调用方不捕获就直接异常上抛CallerRunsPolicy提交线程自己执行不能丢任务且希望自动降速提交线程被拖慢可能拖垮上游DiscardPolicy静默丢弃极少使用静默丢数据最难排查DiscardOldestPolicy丢队列里最老的再插入新的只关心最新数据的场景如实时指标老任务无声消失生产建议优先CallerRunsPolicy有背压或自定义策略。自定义策略通常长这样——先记录再降级而不是丢得无声无息newRejectedExecutionHandler(){OverridepublicvoidrejectedExecution(Runnabler,ThreadPoolExecutorexecutor){// 1) 打点计数便于告警rejectCounter.increment();log.warn(task rejected, poolSize{}, queueSize{},executor.getPoolSize(),executor.getQueue().size());// 2) 降级落盘/入 MQ 重试或直接在调用方同步执行if(!executor.isShutdown()){r.run();// 退化成同步执行至少不丢}}}五、核心线程数怎么定两个公式 一个必做的动作CPU 密集型加解密、复杂计算N 1N CPU 核心数多一个线程用来填补偶发的页缺失停顿。IO 密集型RPC、DB、缓存理论公式N × (1 等待时间 / 计算时间)。落到实践更好用的是从 QPS 反推线程数 QPS × 平均响应时间(秒) × 冗余系数(建议 1.2~2)例接口 QPS 200平均耗时 50ms → 200 × 0.05 10乘 1.5 冗余 ≈15 个线程。必做的一个动作这个估算值只是起点上线后要盯着getActiveCount()和getQueue().size()调。队列长期为空、活跃数远低于核心数 → 可以减队列持续堆积 → 加线程或找下游慢在哪很可能是 DB 慢查询加线程只是把压力转嫁给数据库。监控埋点建议至少在日志里定期输出这三个数log.info(pool: active{}, poolSize{}, queue{}, completed{},pool.getActiveCount(),pool.getPoolSize(),pool.getQueue().size(),pool.getCompletedTaskCount());六、还有三个容易忽略的点优雅关闭shutdown()是「不再接新任务等已提交的跑完」shutdownNow()是「打断所有线程 返回未执行的任务列表」。应用下线时正确写法pool.shutdown();if(!pool.awaitTermination(30,TimeUnit.SECONDS)){pool.shutdownNow();// 超时还没完强制中断}参数可以动态改setCorePoolSize()/setMaximumPoolSize()在运行期生效核心数调小时多余线程会在下次空闲后回收。做配置中心动态调参不用重启应用这是生产上非常实用的一招。线程池不是越多越好每个池子都占线程栈内存、都要维护一个队列。一个应用里到处new ThreadPoolExecutor、几十个池子互相抢 CPU是比单池参数调错更常见的问题。按业务边界收敛池子数量一般一个进程控制在个位数。七、小结按重要性排线程池真正需要记住的是这四句执行顺序是「核心线程 → 队列 → 扩容到最大 → 拒绝」所以队列类型决定了maximumPoolSize有没有意义。永远用有界队列Executors那几个工厂方法Fixed / Single / Cached要么 OOM 要么打爆线程数。submit会吞异常要么get()要么任务体里自己兜住。CallerRunsPolicy的价值是背压而不只是「多跑一个任务」自定义拒绝策略要留打点和降级路径。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。