
目录一、Java 线程池的继承结构常用的是哪种ForkJoinPool 什么场合用二、线程池是什么为什么要用二、核心组件ThreadPoolExecutor 的七大参数① corePoolSize核心线程数② maximumPoolSize最大线程数③ keepAliveTime空闲存活时间④ TimeUnit时间单位⑤ workQueue任务队列⑥ threadFactory线程工厂⑦ RejectedExecutionHandler拒绝策略三、任务执行流程面试必问四、四种快捷创建方式不推荐五、实战从零创建一个线程池手把手第一步建一个线程池第二步理解每个任务怎么跑的第三步验证线程池状态六、线程池的五种状态七、execute() 和 submit() 的区别八、如何设置线程池大小面试高频一、阿里手册确实禁止二、线程池用的是谁的资源三、那微服务里线程池到底是谁的一句话九、面试话术汇总Q1线程池的七大参数是什么执行流程是怎样的Q2为什么阿里巴巴禁止使用 Executors 创建线程池Q3核心线程数怎么设置Q4shutdown() 和 shutdownNow() 的区别Q5线程池的拒绝策略有哪些什么时候用哪种Q6线程池里的线程什么时候被销毁Q7 execute 和 submit 哪个好Q8线程池监控怎么做Q9线程池提交任务时核心线程都在忙、队列也满了这时是先创建临时线程还是先拒绝Q10subimit 和 execute 异常丢失具体指什么面试刁钻题 1线程池中的线程抛出异常会怎样这个线程会被销毁吗面试刁钻题 2用 submit 提交future.get() 阻塞等待时这个线程是否算在活跃线程数里面试刁钻题 3线程池的 prestartAllCoreThreads() 是干什么的预热有什么用面试刁钻题 4一个线程池的 corePoolSize0maximumPoolSize10队列是 SynchronousQueue会怎么样这和 newCachedThreadPool 有什么不同面试刁钻题 5线程池的队列满了后新任务是立即创建临时线程执行还是等队列任务被取走一个再执行面试刁钻题 6线程池什么时候从队列里取任务面试刁钻题 7线程池的线程做完任务后是直接销毁还是怎么回到池子里十、线程池的三个核心问题10.1 什么时候用10.2 什么时候别用10.3 什么时候注意 ThreadLocal十一、一句话速记一、Java 线程池的继承结构整个线程池体系从顶层接口到底层实现四个层级Executor顶级接口 │ 只有一个方法void execute(Runnable command) │ 简单说你给我一个任务我帮你跑 │ └── ExecutorService子接口 │ 扩展了三大能力 │ ① 提交有返回值的任务submit(Callable) │ ② 关闭线程池shutdown/shutdownNow │ ③ 批量提交invokeAll/invokeAny │ ├── ThreadPoolExecutor最常用的实现类 │ 普通阻塞队列线程池七大参数在这里定义 │ 用来执行大部分日常任务 │ └── ForkJoinPool分支合并线程池 工作窃取算法专治分治任务把大任务拆小 Java 8 的 parallelStream 底层就是这个常用的是哪种你 90% 的场景用 ThreadPoolExecutor。// 最常用的是这个 ThreadPoolExecutor executor new ThreadPoolExecutor( 2, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(100) );ForkJoinPool 什么场合用// 大任务拆小计算 1 到 100 万的和 // RecursiveTask 或 RecursiveAction // parallelStream 底层就是 ForkJoinPool int sum IntStream.range(1, 1_000_000).parallel().sum(); // ForkJoinPool.commonPool() 默默在干活和 ThreadPoolExecutor 的核心区别ThreadPoolExecutorForkJoinPool队列一个公共任务队列每个线程一个自己的队列任务获取从公共队列取先拿自己的自己的空了去偷别人的适用场景通用任务执行分治任务大拆小一般面试只需要记住Executor → ExecutorService → ThreadPoolExecutor / ForkJoinPool这个继承结构用的最多的就是 ThreadPoolExecutorForkJoinPool 知道是 parallelStream 的底层就行了。二、线程池是什么为什么要用线程池就是一个装线程的池子里面有若干个已经创建好的线程等着你来派活。没有线程池时 来一个任务 → new 一个线程 → 用完就扔掉销毁 → 频繁创建/销毁线程性能差 有线程池时 先创建一个池子里面放 5 个线程 来一个任务 → 从池子里拿一个空闲线程去执行 → 执行完线程回到池子 → 线程复用减少创建销毁开销为什么需要它问题线程池怎么解决频繁创建/销毁线程CPU 开销大线程复用创建一次用多次线程数量无限制可能 OOM控制最大线程数超出的排队没法管理线程统一管理监控线程状态、任务队列系统负载不可控任务太多时排队或拒绝二、核心组件ThreadPoolExecutor 的七大参数Java 的线程池核心实现是ThreadPoolExecutor它要用到七个参数来构造面试必背public ThreadPoolExecutor( int corePoolSize, // ① 核心线程数 int maximumPoolSize, // ② 最大线程数 long keepAliveTime, // ③ 空闲线程存活时间 TimeUnit unit, // ④ 时间单位 BlockingQueueRunnable workQueue, // ⑤ 任务队列 ThreadFactory threadFactory, // ⑥ 线程工厂 RejectedExecutionHandler handler // ⑦ 拒绝策略 )① corePoolSize核心线程数常驻线程的数量即使这些线程闲着也不销毁除非设置了allowCoreThreadTimeOut。// 假如设 corePoolSize 5线程池里永远至少有 5 个线程活着 // 来 3 个任务 → 用其中 3 个线程剩下 2 个空闲 // 来 10 个任务 → 5 个线程全跑满了② maximumPoolSize最大线程数线程池最多能有的线程数。当核心线程都忙且队列也满了就会创建新的临时线程直到这个数。// 假如 corePoolSize5, maximumPoolSize10, 队列容量100 // 来了 200 个任务 → 5 个核心线程先跑 // 5 个跑满了 → 后面 100 个排队 // 队列也满了 → 再来任务就创建临时线程最多到 10 个 // 10 个线程也满了 → 触发拒绝策略③ keepAliveTime空闲存活时间临时线程空闲了能活多久。超时没接到新任务就销毁。// 假如 keepAliveTime 60 秒 // 临时线程干完活后没事干等 60 秒 → 自动销毁释放资源④ TimeUnit时间单位TimeUnit.SECONDS // 秒 TimeUnit.MILLISECONDS // 毫秒 TimeUnit.MINUTES // 分钟⑤ workQueue任务队列核心线程满了新任务先排队。排队也满了才创建临时线程。常见队列队列特点使用场景ArrayBlockingQueue有界队列容量固定常用能防止 OOMLinkedBlockingQueue可设容量的链表队列默认无界Integer.MAX_VALUE小心 OOMSynchronousQueue容量为 0不排队直接交给线程newCachedThreadPool 用这个PriorityBlockingQueue支持优先级排序需要优先处理某类任务时DelayQueue延迟执行定时任务场景⑥ threadFactory线程工厂给线程取名字的。默认名字是pool-1-thread-1出问题你根本看不出来这是哪个池子的。// ✅ 正确做法自定义名字 ThreadFactory namedFactory new ThreadFactory() { private final AtomicInteger counter new AtomicInteger(1); Override public Thread newThread(Runnable r) { Thread t new Thread(r); t.setName(order-service-pool- counter.getAndIncrement()); return t; } };⑦ RejectedExecutionHandler拒绝策略线程池忙不过来线程满了、队列满了时怎么办。策略行为AbortPolicy默认抛RejectedExecutionExceptionCallerRunsPolicy谁提交的任务谁自己执行在提交者的线程里跑DiscardPolicy静默丢弃不抛异常DiscardOldestPolicy丢弃队列里最老的那个任务再试一次// 拒绝策略一般用 CallerRunsPolicy 最安全 // 因为提交者比如 Tomcat 线程执行起到了反压作用 new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(100), new ThreadPoolExecutor.CallerRunsPolicy()); // 这样至少任务不会丢只是慢一点三、任务执行流程面试必问这是线程池最核心的知识点七步递进提交一个任务 │ ▼ ① 当前线程数 corePoolSize ├── 是 → 创建新线程执行任务 └── 否 → 进入下一步 │ ▼ ② 尝试入队workQueue.offer(task) ├── 入队成功 → 任务在队列里等着线程空闲时取出来执行 └── 入队失败 → 进入下一步队列满了比如 ArrayBlockingQueue(100) 满了 │ ▼ ③ 当前线程数 maximumPoolSize ├── 是 → 创建新临时线程执行任务 └── 否 → 执行拒绝策略 handler.rejectedExecution(task)简化速记版核心线程跑 → 满了去排队 → 队满了扩线程 → 扩满了就拒四、四种快捷创建方式不推荐Java 提供了Executors工具类一行代码创建线程池——但阿里巴巴手册明确禁止// ① 固定线程数 ExecutorService fixed Executors.newFixedThreadPool(5); // 问题LinkedBlockingQueue 无界最大 Integer.MAX_VALUE // 任务积压 → OOM // ② 单线程 ExecutorService single Executors.newSingleThreadExecutor(); // 同上无界队列 // ③ 缓存线程池 ExecutorService cached Executors.newCachedThreadPool(); // 问题maxPoolSize Integer.MAX_VALUE // 请求量大时 → 创建无限多线程 → OOM // ④ 定时任务 ScheduledExecutorService scheduled Executors.newScheduledThreadPool(5); // 同上maxPoolSize 无限阿里巴巴为什么禁止创建方式风险后果newFixedThreadPool无界队列任务积压导致 OOMnewCachedThreadPool无限线程线程创建太多导致 OOMnewSingleThreadExecutor无界队列同 fixednewScheduledThreadPool无限线程同 cached正确做法自己 new ThreadPoolExecutor 显式传参五、实战从零创建一个线程池手把手第一步建一个线程池import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; public class ThreadPoolDemo { public static void main(String[] args) { // 1. 创建线程池 ThreadPoolExecutor executor new ThreadPoolExecutor( 2, // corePoolSize 2 4, // maximumPoolSize 4 60, // keepAliveTime 60 秒 TimeUnit.SECONDS, new ArrayBlockingQueue(3), // 队列容量 3 new NamedThreadFactory(order-pool), // 自定义线程名 new ThreadPoolExecutor.CallerRunsPolicy() // 提交者自己执行 ); // 2. 提交 8 个任务 for (int i 1; i 8; i) { int taskId i; executor.execute(() - { System.out.println(Thread.currentThread().getName() 正在执行任务 taskId); try { Thread.sleep(2000); // 模拟任务执行 2 秒 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() 执行完任务 taskId); }); } // 3. 关闭线程池 executor.shutdown(); } // 自定义线程工厂给线程取个好名字 static class NamedThreadFactory implements ThreadFactory { private final String prefix; private final AtomicInteger counter new AtomicInteger(1); public NamedThreadFactory(String prefix) { this.prefix prefix; } Override public Thread newThread(Runnable r) { Thread t new Thread(r); t.setName(prefix - counter.getAndIncrement()); return t; } } }第二步理解每个任务怎么跑的核心池2 个线程order-pool-1, order-pool-2 队列3 个位置 最大线程4 个 提交任务 1 → order-pool-1 执行没到 corePoolSize创建新线程 提交任务 2 → order-pool-2 执行 提交任务 3 → 入队核心线程满了 提交任务 4 → 入队 提交任务 5 → 入队 提交任务 6 → 队列满了创建临时线程 order-pool-3 执行 提交任务 7 → 队列满了创建临时线程 order-pool-4 执行 提交任务 8 → 线程数到 maximumPoolSize4 了触发拒绝策略CallerRunsPolicy → main 线程自己执行 最终order-pool-1, -2, -3, -4 main 线程 5 个线程同时执行第三步验证线程池状态// 提交完任务后打印线程池状态 System.out.println( 线程池状态 ); System.out.println(活跃线程数 executor.getPoolSize()); System.out.println(核心线程数 executor.getCorePoolSize()); System.out.println(队列中任务数 executor.getQueue().size()); System.out.println(已完成任务数 executor.getCompletedTaskCount());六、线程池的五种状态shutdown() RUNNING ──────────────→ SHUTDOWN │ 不收新任务跑完队列里的 │ │ │ shutdownNow() │ ├────────────────────→ STOP │ 不收新任务也不跑队列中断正在跑的 │ │ │ │ 队列空 所有线程终止 │ ▼ │ TIDYING │ │ │ │ terminated() 钩子执行完 │ ▼ └──────────────────── TERMINATEDexecutor.shutdown(); // 优雅关闭等队列跑完 executor.shutdownNow(); // 暴力关闭中断正在跑的返回队列里的任务 executor.isShutdown(); // 是否已关闭 executor.isTerminated(); // 是否已完全终止 executor.awaitTermination(10, TimeUnit.SECONDS); // 等待线程池关闭最多等 10 秒面试常问shutdown 和 shutdownNow 区别shutdown不接受新任务但会等队列里已有的任务执行完再关闭。shutdownNow不接受新任务正在执行的线程调 interrupt()队列里的任务直接返回不执行了。七、execute() 和 submit() 的区别// execute提交 Runnable没有返回值异常直接打印 executor.execute(() - { System.out.println(没有返回值); }); // submit提交 Callable返回 Future 可以拿结果或异常 FutureString future executor.submit(() - { return 有返回值; }); String result future.get(); // 阻塞等待结果 // 如果 submit 的任务抛异常future.get() 会抛出 // 而 execute 的异常直接打到控制台你可能不知道面试常问// execute 异常丢失问题 executor.execute(() - { throw new RuntimeException(报错了); // 控制台打印没人知道 }); // submit 异常 Future? future executor.submit(() - { throw new RuntimeException(报错了); }); future.get(); // 抛 ExecutionException你能捕获到结论生产环境用 submit不要用 execute至少异常不会无声无息丢。八、如何设置线程池大小面试高频公式CPU 密集型任务大量计算 线程数 CPU 核数 1 IO 密集型任务大量网络/磁盘等待 线程数 CPU 核数 × 2 或更大 更精确的公式美团方案 线程数 CPU核数 × 1 等待时间 / 计算时间 // 获取 CPU 核数 int cpus Runtime.getRuntime().availableProcessors(); // CPU 密集型 ThreadPoolExecutor cpuPool new ThreadPoolExecutor( cpus 1, cpus 1, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(1000) ); // IO 密集型比如调远程服务 ThreadPoolExecutor ioPool new ThreadPoolExecutor( cpus * 2, cpus * 2, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(1000) );为什么 CPU 密集型不能太多假如 4 核 CPU开 8 个线程1 个线程在跑计算其他 7 个在抢 CPU线程切换本身就要消耗 CPU 时间反而比 4 个线程跑得更慢为什么 IO 密集型可以多线程在等数据库/远程服务返回时CPU 是空闲的更多的线程可以让 CPU 在等待期间干别的事不浪费 CPU 时间片九、面试话术汇总Q1线程池的七大参数是什么执行流程是怎样的话术七大参数是 corePoolSize、maximumPoolSize、keepAliveTime、TimeUnit、workQueue、threadFactory、RejectedExecutionHandler。执行流程是先看核心线程有没有满没满就创建核心线程满了就入队队满了就创建临时线程直到最大线程数再满就执行拒绝策略。Q2为什么阿里巴巴禁止使用 Executors 创建线程池话术三个原因。newFixedThreadPool 和 newSingleThreadExecutor 用了无界的 LinkedBlockingQueue队列容量是 Integer.MAX_VALUE任务积压会导致 OOM。newCachedThreadPool 的最大线程数是 Integer.MAX_VALUE高并发下会创建无限线程也导致 OOM。而且默认的线程名没有业务含义出问题不好排查。所以推荐自己 new ThreadPoolExecutor 显式传参。Q3核心线程数怎么设置话术分 CPU 密集型和 IO 密集型。CPU 密集型设 CPU 核数1避免线程切换浪费 CPU。IO 密集型设 CPU 核数×2 或更大因为线程在等待 IO 时 CPU 是空闲的。更精确的公式是线程数 CPU核数 × (1 等待时间/计算时间)但实际还是需要通过压测来验证。Q4shutdown() 和 shutdownNow() 的区别话术shutdown 是优雅关闭不再接收新任务但会执行完队列里已有的任务。shutdownNow 是暴力关闭不再接收新任务正在执行的任务调 interrupt() 中断队列里还没执行的任务直接返回。生产环境优先用 shutdown除非需要快速释放资源。Q5线程池的拒绝策略有哪些什么时候用哪种话术四种。AbortPolicy默认抛异常、CallerRunsPolicy谁提交谁执行、DiscardPolicy静默丢弃、DiscardOldestPolicy丢最老任务再试。生产环境最推荐 CallerRunsPolicy因为让提交者自己执行起到了反压作用不会丢任务只是请求变慢。AbortPolicy 如果调用方没处理好异常任务就丢了。Q6线程池里的线程什么时候被销毁话术核心线程默认不销毁除非设置了 allowCoreThreadTimeOut(true)。非核心线程超过 corePoolSize 创建的那些在空闲时间超过 keepAliveTime 后自动销毁。所以如果任务量降下来了临时线程会自动回收释放资源。Q7 execute 和 submit 哪个好话术生产环境用 submit。execute 提交的任务抛异常只会打印到控制台不会通知调用方容易造成异常静默丢失。submit 返回 Future调用 future.get() 会抛 ExecutionException能捕获并处理异常。而且 submit 支持 Callable 有返回值。Q8线程池监控怎么做话术通过 ThreadPoolExecutor 自带的 getPoolSize()、getActiveCount()、getQueue().size()、getCompletedTaskCount() 等方法获取实时状态。还可以通过 beforeExecute 和 afterExecute 钩子方法做埋点记录每个任务的执行耗时。生产环境一般对接 Prometheus Grafana 做可视化监控。Q9线程池提交任务时核心线程都在忙、队列也满了这时是先创建临时线程还是先拒绝话术先创建临时线程。完整流程是① 核心线程 corePoolSize → 创建② 尝试入队③ 入队失败队列满了且线程数 maximumPoolSize → 创建临时线程④ 线程数已到 maximumPoolSize → 执行拒绝策略。Q10subimit 和 execute 异常丢失具体指什么话术execute 的任务如果抛了 RuntimeException线程池会捕获后打印到 System.err但调用方完全不知道异常静默丢失。submit 的任务抛异常异常会被包装在 Future 里调用 future.get() 时会抛 ExecutionException调用方可以 catch 到并处理。所以线上代码建议用 submit 处理 future。面试刁钻题 1线程池中的线程抛出异常会怎样这个线程会被销毁吗话术会。Worker 线程在执行任务时如果抛异常runWorker 方法会捕获到执行 afterExecute 钩子然后线程退出。线程池会判断当前线程数是否小于核心线程数如果是就创建一个新 Worker 补充进来。所以线程池并不会因为这个线程挂了就一直少一个线程它会自动补充到 corePoolSize。面试刁钻题 2用 submit 提交future.get() 阻塞等待时这个线程是否算在活跃线程数里话术算的。future.get() 阻塞的是调用 get() 的那个线程通常是业务主线程不是线程池里的 Worker 线程。Worker 线程在任务执行完后已经回到线程池了。get() 阻塞的是外面的线程不是池子里的线程池子里的活跃线程数已经是空闲状态了。面试刁钻题 3线程池的 prestartAllCoreThreads() 是干什么的预热有什么用话术默认情况下核心线程是懒加载的来第一个任务才创建。prestartAllCoreThreads() 会提前创建所有核心线程。预热的作用是避免第一个请求因为线程创建而变慢常用于对延迟敏感的系统。比如网关服务启动时先预热好线程第一个请求来的时候线程已经存在了。面试刁钻题 4一个线程池的 corePoolSize0maximumPoolSize10队列是 SynchronousQueue会怎么样这和 newCachedThreadPool 有什么不同话术SynchronousQueue 队列容量为 0不存储任务必须直接交给线程。因为 corePoolSize0来任务时会先尝试创建临时线程。这与 newCachedThreadPool 行为类似区别是 newCachedThreadPool 的 keepAliveTime60s临时线程存活 60 秒而如果这里自己设 corePoolSize0 SynchronousQueue keepAliveTime0那临时线程干完活就立刻销毁了几乎等于来一个任务就 new 一个线程、用完就销毁性能极差。面试刁钻题 5线程池的队列满了后新任务是立即创建临时线程执行还是等队列任务被取走一个再执行话术立即执行。offer() 入队失败返回 false不会等队列有位置。offer 失败后走 addWorker() 加新线程并直接提交任务。所以当任务队列满的时候新来的任务是由临时线程立即执行的不会排队等待队列空出来。面试刁钻题 6线程池什么时候从队列里取任务话术Worker 线程执行完当前任务后会去 workQueue.poll() 或 take() 取下一个任务。Worker 的 runWorker 方法是死循环不断取任务执行。取不到就超时后退出如果是临时线程或者一直阻塞等如果是核心线程。所以队列里的任务不是主动被推出去的而是线程空下来了自己拉的。面试刁钻题 7线程池的线程做完任务后是直接销毁还是怎么回到池子里话术做完任务后Worker 线程进入无限循环取下一个任务 → 执行 → 取下一个 → 执行。如果取不到任务分两种情况核心线程调 workQueue.take() 阻塞等一直活着临时线程调 workQueue.poll(keepAliveTime, TimeUnit)超时后还没取到就退出销毁。所以线程做完任务并没有回到池子这个概念它本来就在池子里只是从忙碌状态变成了等任务状态。十、线程池的三个核心问题10.1 什么时候用场景要不要线程池异步执行耗时操作发短信、发邮件✅ 要多任务并行处理同时查询多个服务✅ 要定时/周期任务✅ 要Tomcat 接收 HTTP 请求✅ Tomcat 自己已经用了线程池你不需要自己建同步调一个方法❌ 不需要简单的 for 循环❌ 不需要10.2 什么时候别用// ❌ 滥用方法内部创建线程池调用一次就创建一个 public void doSomething() { ExecutorService pool Executors.newFixedThreadPool(5); // 每次调都 new pool.submit(() - { ... }); pool.shutdown(); } // 这个和每次 new Thread() 没什么区别 // ✅ 正确线程池是全局的 public class OrderService { private static final ExecutorService POOL createPool(); public void doSomething() { POOL.submit(() - { ... }); } }10.3 什么时候注意 ThreadLocal// 线程池里的线程是复用的 // 如果你用了 ThreadLocal用完不清除 // 下一个任务用同一个线程时还能拿到上一个任务的数据 public void handleRequest() { try { // 设置用户信息 UserContext.set(userId); executor.submit(() - { // 这里拿到的可能是别人的 userId System.out.println(UserContext.get()); }); } finally { // ⚠️ 必须清除 UserContext.clear(); } }解决方案用阿里开源的transmittable-thread-local或者在回调里显式传参。十一、一句话速记七个参数背下来核心最大存活时队工艺拒 核心线程、最大线程、存活时间、时间单位、队列、线程工厂、拒绝策略 执行流程四步走 核心跑 → 队排队 → 扩到最大 → 拒了 参数设多少 CPU密集 CPU核数1 IO密集 CPU核数×2 起步 别用Executors手写TPE 队列用有界工厂要命名 拒绝CallerRuns异常别丢掉