面试题库:线程池核心参数与工作原理
题目请介绍 Java 线程池的核心参数与工作原理。参考答案前言线程池这道题在面试中的出场率堪比请自我介绍一下。不夸张地说只要简历上写了熟悉多线程面试官十有八九会顺着这个话题往下挖。但很多同学的回答停留在七个参数背一遍的水平——说实话面试官听这种答案已经听到耳朵起茧了。这篇文章的目标是让你不仅知道参数是啥还能讲清楚线程池内部到底在干什么给面试官一种这人是真用过的感觉。一、为什么要有线程池——先搞清楚它解决什么问题在聊参数之前我们先花一分钟搞清楚没有线程池的世界有多痛苦。// 没有线程池的写法——来一个任务就 new 一个线程 new Thread(() - { // 处理任务... }).start();这种写法有三个致命问题创建和销毁开销大每次new Thread()都要分配内存、初始化栈用完还得 GC 回收。任务多的时候光创建/销毁线程的时间可能比真正干活的时间还长。线程数不可控如果瞬间来了 10000 个请求你就创建 10000 个线程。操作系统可不是慈善家——线程太多会导致 CPU 频繁上下文切换甚至直接把系统拖垮。线程缺乏管理哪天想给所有线程发个别干了下班的信号你都不知道它们人在哪。线程池就像一个外包公司甲方你派活儿外包公司线程池负责招人、派人、管人、开人。你只管把需求扔过去不用操心人员管理。二、认识葫芦娃七兄弟——七大核心参数Java 的ThreadPoolExecutor有七个参数我习惯叫它们葫芦娃七兄弟。先一锅端上来给你看看public ThreadPoolExecutor( int corePoolSize, // 老大核心线程数 int maximumPoolSize, // 老二最大线程数 long keepAliveTime, // 老三空闲存活时间 TimeUnit unit, // 老四时间单位 BlockingQueueRunnable workQueue, // 老五工作队列任务排队的地方 ThreadFactory threadFactory, // 老六线程工厂怎么造线程 RejectedExecutionHandler handler // 老七拒绝策略活太多干不完怎么办 )别慌参数虽然多但每个都有它独特的人设我们一个一个来认识。老大corePoolSize核心线程数专业说法线程池中长期保持的线程数量即使它们闲着也不会被回收除非设置了allowCoreThreadTimeOut。人话版外包公司的正式员工。就算没活干也养着他们工资照发。因为一旦来活了正式员工能立刻上手不用走招聘流程。老二maximumPoolSize最大线程数专业说法线程池中允许存在的最大线程数量。人话版外包公司能容纳的最大人数正式 外包。核心线程忙不过来任务队列也满了那就只能临时招外包人员了。但公司工位有限不能无限招人。老三 老四keepAliveTimeunit空闲存活时间 时间单位专业说法当线程数超过核心线程数时多余的空闲线程在终止前等待新任务的最长时间。人话版外包人员的合同期。活干完了外包人员闲着没事干等keepAliveTime这么久还没新活儿就解除合同走人。正式员工不受这个限制。// 例核心线程 5 个最大线程 10 个 // 那多出来的 5 个线程空闲超过 60 秒就会被回收 new ThreadPoolExecutor(5, 10, 60L, TimeUnit.SECONDS, workQueue);老五workQueue工作队列 / 阻塞队列专业说法用于存放待执行任务的阻塞队列。人话版公司的待办事项清单。任务来了核心线程都在忙那就先把任务记在本子上排队。这是线程池的精髓所在——能排队就别招人能排队就别拒绝。常用的三种队列队列类型特点适用场景SynchronousQueue不存任务直接交接。提交一个任务必须有一个线程来接没有就新建线程或拒绝希望任务立即执行不想排队的场景如Executors.newCachedThreadPool()LinkedBlockingQueue无界可以无限排队其实是Integer.MAX_VALUE任务永远进不了招外包和拒绝的流程任务量平稳、不会爆内存的场景如Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor()ArrayBlockingQueue有界队列有固定容量满了才触发招外包→拒绝流程生产环境推荐使用能防止内存溢出老六threadFactory线程工厂专业说法用于创建新线程的工厂可以自定义线程名、优先级、是否为守护线程等。人话版人力资源部规定怎么招人、人叫什么名字。ThreadFactory namedThreadFactory new ThreadFactoryBuilder() .setNameFormat(order-pool-%d) // 线程名order-pool-1, order-pool-2... .build();给线程起个好名字太重要了——出问题时你看着pool-1-thread-3这种默认名字完全不知道是哪个业务在跑。换成order-pool-3一眼就定位到了订单模块。老七handler拒绝策略专业说法当任务队列已满且线程数达到最大值时对新提交任务的拒绝处理策略。人话版公司满员、本子写满——再来活儿怎么办这就是最后一道防线。JDK 内置四种拒绝策略策略行为一句话总结AbortPolicy默认直接抛RejectedExecutionException干不了滚然后甩你一个异常CallerRunsPolicy让提交任务的线程自己执行这个任务我干不了你自己干吧DiscardPolicy默默丢弃不抛异常把任务扔进垃圾桶假装没看见DiscardOldestPolicy丢弃队列里最老的任务然后重试提交把最早的活儿挤掉换新的上生产环境建议一般用CallerRunsPolicy让提交方自己跑相当于有了个隐形的限流拖慢提交速度给线程池喘息的空间。三、工作原理——线程池到底怎么运转的讲完参数现在是重头戏当一个任务被提交到线程池它到底经历了什么直接上一张任务提交三部曲流程图用代码来描述就是// ThreadPoolExecutor.execute() 的简化版逻辑 public void execute(Runnable command) { // 第一步看看核心线程有没有闲着的 if (workerCount corePoolSize) { addWorker(command); // 直接招个人来干 return; } // 第二步核心线程都在忙把任务放进队列排队 if (workQueue.offer(command)) { // offer 不阻塞放进去就返回 true // 入队成功任务在队列里等着被消费 // 但这里还要做一次双重检查——万一入队后线程池关闭了呢 if (isShutdown()) { workQueue.remove(command); // 从队列移除 reject(command); // 走拒绝策略 } return; } // 第三步队列满了试试能不能招外包创建临时线程 if (workerCount maximumPoolSize) { addWorker(command); // 招个外包来干 return; } // 第四步外包也招满了拒了 reject(command); }用人话串一遍就是你调用方去外包公司提交任务先看看正式员工核心线程有没有闲着的有就直接安排。正式员工都在忙那就把任务记在本子上工作队列等谁忙完了自己来取。本子也记满了那就临时招个外包新建线程但不超过最大线程数来干活。外包也招满了那只能对不起了——按拒绝策略处理通常是甩个异常或者让你自己干。核心口诀先核心再排队再外包最后拒绝。记住这四个步骤线程池的工作流程就忘不掉了。四、四个常见的坑人线程池Java 通过Executors工具类提供了几个快捷创建线程池的方法但**《阿里巴巴 Java 开发手册》强制要求不能用它们来创建线程池**。为什么呢// ❌ 禁止使用 Executors 快捷方法创建线程池 ExecutorService pool1 Executors.newFixedThreadPool(10); ExecutorService pool2 Executors.newCachedThreadPool(); ExecutorService pool3 Executors.newSingleThreadExecutor(); ExecutorService pool4 Executors.newScheduledThreadPool(5); // ✅ 推荐手动指定参数 ExecutorService pool new ThreadPoolExecutor( 10, 20, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(200), new ThreadFactoryBuilder().setNameFormat(biz-pool-%d).build(), new ThreadPoolExecutor.CallerRunsPolicy() );这四个方法各自有什么坑方法内部实现坑在哪里newFixedThreadPool(n)new ThreadPoolExecutor(n, n, 0L, ..., new LinkedBlockingQueue())队列无界Integer.MAX_VALUE任务堆积会撑爆内存newSingleThreadExecutor()new ThreadPoolExecutor(1, 1, 0L, ..., new LinkedBlockingQueue())同上队列无界newCachedThreadPool()new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, ..., new SynchronousQueue())最大线程数无界Integer.MAX_VALUE来一个任务开一个线程CPU 上下文切换直接爆炸newScheduledThreadPool(n)new ScheduledThreadPoolExecutor(n)→ 父类构造器new ThreadPoolExecutor(n, Integer.MAX_VALUE, 0L, ..., new DelayedWorkQueue())最大线程数无界一句话总结要么队列无限要么线程数无限——反正总有一个能把你内存搞崩。五、配置线程数——不是拍脑袋的事面试官可能会追一句如果让你配置一个线程池核心线程数你设多少这个问题没有绝对值但有一个被广泛引用的经验公式CPU 密集型任务计算为主几乎没有 IOCPU 核数 1线程多了也没用CPU 就那么几个核反而增加上下文切换IO 密集型任务网络请求、文件读写、数据库操作CPU 核数 × 2甚至更多IO 操作大部分时间在等待线程可以切出去干活int cpuCores Runtime.getRuntime().availableProcessors(); // CPU 密集型 int poolSize cpuCores 1; // IO 密集型保守 int poolSize cpuCores * 2;但说实话这个公式只是一个起点最终数字还是要结合压测来调。没有任何公式能替代真实环境下的压力测试。六、线程池的生命周期——RUNNING → SHUTDOWN → STOP → TIDYING → TERMINATED线程池有五种状态看一下就行面试中一般问到shutdown()和shutdownNow()的区别就够了// shutdown() — 温柔关门已提交的任务执行完但不接受新任务 pool.shutdown(); // shutdownNow() — 暴力关门尝试中断正在执行的任务返回未执行的任务列表 ListRunnable undone pool.shutdownNow();方法是否接受新任务已在队列中的任务正在执行的任务shutdown()否会执行完会执行完shutdownNow()否不执行返回列表发中断信号interrupt()但线程不一定会停你可以这样记shutdown()像下班铃——大家把手头的活干完再走。shutdownNow()像火灾警报——别干了立刻走人。七、常见面试追问Q1核心线程数为 0 时线程池还能工作吗能。newCachedThreadPool()就是核心线程数为 0 的例子。来了任务直接进SynchronousQueue没有线程接手就新建线程。所有线程在空闲 60 秒后回收极端情况下线程池里一个线程都没有。Q2corePoolSize maximumPoolSize意味着什么意味着线程数固定忙的时候不会招外包是个固定大小线程池。这就是newFixedThreadPool()的做法。Q3核心线程会被回收吗默认不会但如果调用了pool.allowCoreThreadTimeOut(true)核心线程在空闲超过keepAliveTime后也会被回收。Q4任务提交后是先创建非核心线程还是先放队列先放队列再创建非核心线程。这是很多人记反的地方。线程池的设计哲学是能排队就别招人队列实在放不下了才招外包。Q5submit()和execute()有什么区别维度execute()submit()参数类型只接受Runnable接受Runnable和Callable返回值无返回Future?可以拿到结果和异常异常处理未捕获异常会使线程退出异常被封装在Future中通过get()抛出八、一句话带走线程池就是一个精明的外包公司核心员工坐镇任务先排队排不下再招临时工招满了直接拒单。七个参数控制这一切但最重要的就四个——核心数、最大数、队列、拒绝策略。面试回答模板如果你在面试中被问到这道题可以参考下面的口语版回答线程池的核心参数有七个核心线程数、最大线程数、空闲存活时间、时间单位、工作队列、线程工厂和拒绝策略。其中最关键的四个是核心线程数、最大线程数、工作队列和拒绝策略。工作原理上当一个任务提交到线程池时会经历四个步骤第一判断核心线程是否都有空闲有空闲就直接执行第二核心线程都在忙就把任务放入工作队列中排队等待第三如果队列也满了但当前线程数还没达到最大线程数就创建新的线程来执行任务第四如果线程数已经达到最大值且队列也满了那就执行拒绝策略默认是抛出 RejectedExecutionException。生产环境配置线程池时我一般会手动指定参数避免使用 Executors 的快捷方法因为像 newFixedThreadPool 内部用的是无界队列任务堆积会有 OOM 风险。线程数的设置上CPU 密集型的任务我一般设 CPU 核数加一IO 密集型的设 CPU 核数乘以二最终值还是要靠压测来确定。队列我倾向用有界队列比如 ArrayBlockingQueue拒绝策略通常用 CallerRunsPolicy 实现一个隐形的限流效果。