面试官陷阱:动态修改核心线程数,是立即生效的吗?90%的Java开发都栽在这道题上!
最近在看简历的时候发现一个非常有意思的现象现在10个Java开发的简历里有8个都写着“基于 Nacos/Apollo 实现了动态线程池”。前几天我面试了一个工作了5年的兄弟我抛出了一个极其日常的拷问 “既然你做过动态线程池那假设大促前夕你通过配置中心把 corePoolSize 从 10 调大到了 50。请问这新增的 40 个核心线程是立刻就被创建出来在池子里待命的吗还是得等有新任务提交时才创建”他毫不犹豫地回答“因为是动态配置底层调用了 setCorePoolSize()所以是立刻生效立马创建出来的。”听到这个答案Fox 老师只能在心里默默叹气兄弟面试基本走远了。 如果你也这么想说明你不仅没看过 JDK 源码而且绝对没有真正扛过线上的高并发大促。今天Fox 带你扒开 Doug Lea 大神写的底裤看看真正的底层逻辑。一、 源码揭秘别猜了真相藏在 setCorePoolSize 里做技术最忌讳的就是“想当然”。不要猜跟 Fox 直接翻开 java.util.concurrent.ThreadPoolExecutor 的源码。动态调整核心线程数其实分为“调小”和“调大”两种截然不同的处理逻辑。我们重点看核心代码public void setCorePoolSize(int corePoolSize) { if (corePoolSize 0) thrownew IllegalArgumentException(); int delta corePoolSize - this.corePoolSize; this.corePoolSize corePoolSize; // 任务正在执行的情况 if (workerCountOf(ctl.get()) corePoolSize) { // 【场景一调小参数】 // Fox划重点如果当前线程数大于新的核心线程数立刻去中断空闲的线程 interruptIdleWorkers(); } elseif (delta 0) { // 【场景二调大参数】 // 全场最大陷阱预警看清楚这一行 // k 是我们要新增的线程数 和 当前阻塞队列里的任务数 的【最小值】 int k Math.min(delta, workQueue.size()); // 循环尝试添加 Worker (即创建新线程) while (k-- 0 addWorker(null, true)) { // 如果在循环过程中队列空了直接跳出循环 if (workQueue.isEmpty()) break; } } }Fox 源码大白话翻译1. 调小核心线程数 (delta 0)结论立刻生效但很温柔。它调用了 interruptIdleWorkers()。注意它只会去中断那些当前没有任务在手、处于空闲状态的线程。正在吭哧吭哧干活的线程是不会被粗暴干掉的这叫平滑缩容。2. 调大核心线程数 (delta 0) —— 坑死无数人的地方看这行要命的代码int k Math.min(delta, workQueue.size());系统会不会立刻把线程建好等你答案是取决于你的阻塞队列里有没有积压的任务队列为空大促预热场景workQueue.size() 为 0导致 k 为 0。while 循环根本就不进也就是说你把参数调大了但底层根本没有任何线程被创建必须等后续有新任务 execute() 进来才会慢吞吞地建线程。队列有积压底层会立刻去创建新线程帮忙但最多只创建等于队列任务数量的线程。积压干完立刻罢工。二、 大促预热到底该怎么做当年 Fox 做双11大促的时候系统差点因为这个坑挂掉。我们在流量洪峰到来前提前通过 Nacos 把 corePoolSize 调大。结果洪峰一到接口耗时瞬间飙升。为什么因为流量没来时队列是空的线程压根没建等洪峰来了系统开始疯狂向操作系统申请创建线程这种内核级的 Context Switch 开销直接造成了严重的“冷启动抖动”。那怎么才能实现真正的“预热” 其实 JDK 早就在角落里给你准备好了 API只是很多人从没用过prestartAllCoreThreads()。修改完核心参数后补上这一刀强制打满空闲线程待命这才是高并发老司机的操作。三、 Talk is cheap上代码Fox 从来不只吹牛不给代码。下面这段代码直接 Copy 就能跑。自己跑一遍比背 100 遍八股文都管用。import java.util.concurrent.*; /** * 跟着 Fox 验证动态线程池陷阱 * 重点验证调大核心线程数时新线程到底会不会立即创建 */ publicclass DynamicCorePoolSizeDemo { public static void main(String[] args) throws InterruptedException { // 1. 初始化一个小水管核心数 2 ThreadPoolExecutor executor new ThreadPoolExecutor( 2, 20, 60, TimeUnit.SECONDS, new LinkedBlockingQueue(100) ); System.out.println( 初始状态 ); printPoolState(executor); // 2. 模拟大促前夕老板让你把核心线程数干到 10 System.out.println(\n 收到 Nacos 变更修改 CorePoolSize 为 10 ); executor.setCorePoolSize(10); // 停顿 1 秒让子弹飞一会儿 Thread.sleep(1000); // 见证打脸时刻参数虽然是10但真实线程数依然是 0 printPoolState(executor); // 3. Fox 的填坑神技执行预热操作 System.out.println(\n 执行老司机操作prestartAllCoreThreads() ); int prestartedCount executor.prestartAllCoreThreads(); System.out.println(实际提前强制启动的线程数: prestartedCount); Thread.sleep(1000); // 验证成功此时池子里才真正有 10 个空闲线程在待命 printPoolState(executor); executor.shutdown(); } private static void printPoolState(ThreadPoolExecutor executor) { System.out.printf([Fox 监控大盘] 参数设定Core: %d, 真实存在线程数: %d, 正在干活线程数: %d, 队列积压: %d%n, executor.getCorePoolSize(), executor.getPoolSize(), executor.getActiveCount(), executor.getQueue().size()); } }运行结果自己看 初始状态 [Fox 监控大盘] 参数设定Core: 2, 真实存在线程数: 0, 正在干活线程数: 0, 队列积压: 0 收到 Nacos 变更修改 CorePoolSize 为 10 [Fox 监控大盘] 参数设定Core: 10, 真实存在线程数: 0, 正在干活线程数: 0, 队列积压: 0 -- (坑就在这) 执行老司机操作prestartAllCoreThreads() 实际提前强制启动的线程数: 10 [Fox 监控大盘] 参数设定Core: 10, 真实存在线程数: 10, 正在干活线程数: 0, 队列积压: 0四、 面试总结下次再有面试官想在这个问题上套路你请直接把这段话甩在他脸上“动态修改核心线程数调用 setCorePoolSize 后如果是调小平滑缩容立刻中断空闲线程。如果是调大绝对不会立刻瞎建线程它取决于当前队列里有没有积压任务。没有任务它就按兵不动。在真实的生产预热场景中光调大参数是外行做法。修改完参数后必须强制调用 prestartAllCoreThreads() 方法让核心线程提前创建并阻塞在队列上待命以此来消除系统冷启动抖动。这才是完整的动态调优闭环。”