你好我是fengxin_rou这是我的个人主页fengxin_rou的主页❄️欢迎查看我的专栏我的专栏《Java后端学习》、《JAVASE基础》、《JUC并发》、《redis》、《JVM虚拟机》、《MYSQL》、《黑马点评》、《rabbitmq》、《JavaWebAI的talis学习系统》、《苍穹外卖》目录前言1.线程池七大核心参数含义2.线程池工作原理 / 执行流程3. 线程池都有哪些种类4. 四种拒绝策略分别是什么适用场景5. JDK 内置四大线程池Fixed、Cached、Single、Scheduled 各自特点、坑点6. 为什么阿里禁止用 Executors 创建线程池7. 核心线程数怎么合理设置IO 密集型、CPU 密集型公式8. 线程池空闲线程回收机制9. 线程池关闭 shutdown () 和 shutdownNow () 区别10. 线程池任务提交 execute () 和 submit () 区别11. 线程池异常怎么捕获前言本文介绍了JUC并发种线程池相关属性、以及线程池相关种类、回收机制等面试常考问题1.线程池七大核心参数含义corePoolSize:核心线程容量大小线程池中线程的数量如果 corePoolSize那么即使这些线程处于空闲状态也不会被销毁。maximumPoolSize最大线程容量大小限制了线程池能创建的最大线程总数包括核心线程和非核心线程当阻塞队列也就是workQueue已满并且数量最大线程总数以内的话会创建新线程来处理任务。如果两者都满了则会触发handler拒绝策略keepAliveTime超过corePoolSize数量的线程在空闲状态能存活的时长unitkeepAliveTime的时间单位workQueue工作队列当没有空闲的线程执行新任务时该任务就会被放入工作队列中等待执行。threadFactory线程工作工厂可以用来修改线程名字handler拒绝策略当线程数已达maximumPoolSize且工作队列已满时对新提交任务的处理策略如直接抛出异常、由提交任务的线程执行等2.线程池工作原理 / 执行流程首先提交任务提交任务之后判断线程数量是否小于核心线程数若小于则直接执行任务若大于则进入阻塞队列阻塞队列若没满则等待空闲线程执行任务满了就需要创建非核心线程来执行任务若线程池满了就触发拒绝策略3.线程池都有哪些种类ScheduledThreadPool:可以设置定期的执行任务它支持周期性或定时任务比如每隔10秒执行一次任务FixedThreadPool它的核心线程数即最大线程数它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外之后的线程数量就是固定的。当任务数超过核心线程数不会创建新的线程执行任务而是把线程加入以LinkedBlockingQueue为底层的workQueue它的特点是无界(设置的Integer.MAX_VALUE),当消费速度跟不上生产速度时会导致消息堆积最终导致发生OOM这是阿里手册禁用Executors.newFixedThreadPool()的主要原因CachedThreadPool:又称缓存线程池特点在于理论上没有线程池容量限制(maximumPoolSize Integer.MAX_VALUE)当线程限制60秒后会被回收。底层以SychronousQueue为workQueue特点是没有容量直接中转或传递任务每来一个新的任务就会创建一个新的线程来执行。所以在高并发瞬时大量任务的情况下CachedThreadPool会创建上千个线程这样很可能把系统资源耗尽导致OOM这也是阿里手册禁用的原因实际情况请手动new ThreadPool并设计最大容量SingleThreadExecutor该线程池只有一个线程并且只使用这唯一的线程去执行任务如果线程池仅有的一个线程在执行任务中发生异常那么线程池会创建一个新的线程去执行这剩下的任务。因为只有一个线程所以适合需要按被提交顺序去依次执行的场景。而前面的不行因为前面的线程有多个并且并行执行SingleThreadScheduledExecutor也就是ScheduledThreadPool的单一线程版只有一个线程能用其余特性和ScheduledThreadExecutor一样4.四种拒绝策略分别是什么适用场景分别是callerPolicy、abortPolicy、discardPolicy、discardOldestPolicycallerPolicy让调用这个任务的线程去执行这个被拒绝的任务除非线程池停止或者线程池的任务队列已有空缺abortPolicy直接抛出任务被线程池拒绝的异常discardPolicy不做任何处理静默拒绝任务discardOldestPolicy抛弃最老的任务来执行当前任务5.JDK 内置四大线程池Fixed、Cached、Single、Scheduled 各自特点、坑点5.1FixedThreadPool 固定线程池核心特点核心线程数 最大线程数线程数量固定队列无界队列LinkedBlockingQueue容量 Integer.MAX_VALUE约 21 亿线程空闲不会被回收长期驻留适用于任务量已知、稳定、负载均匀的场景致命坑点无界队列会无限堆积任务高并发下瞬间占满内存直接 OOM队列永远不会满所以最大线程数永远不会生效拒绝策略也永远不会触发一句话总结固定线程数但队列无限大 → 任务堆积 OOM5.2CachedThreadPool 缓存线程池核心特点核心线程数 0最大线程数 Integer.MAX_VALUE无限队列SynchronousQueue容量 0不存任务直接移交来一个任务就创建一个线程空闲 60s 自动销毁适用于大量短生命周期、轻量级任务致命坑点高并发、任务提交速度 处理速度时无限创建线程线程过多会耗尽 CPU、内存、文件句柄 → OOM / 系统卡死一句话总结不排队、无限创建线程 → 线程爆炸 OOM5.3SingleThreadExecutor 单线程线程池核心特点核心线程 最大线程 1永远只有一个线程工作无界队列LinkedBlockingQueue保证任务严格按提交顺序串行执行线程意外终止会自动重建一个坑点单线程串行执行并发能力极差吞吐量低同样因为无界队列任务堆积会 OOM一个任务阻塞 / 异常会影响后面所有任务一句话总结单线程串行、无界队列 → 效率低 任务堆积 OOM5.4ScheduledThreadPool 定时 / 周期线程池核心特点支持定时执行、周期重复执行如每隔 5 秒执行一次队列DelayedWorkQueue延迟队列SingleThreadScheduledExecutor单线程版本坑点周期任务抛出异常且未捕获会直接停止调度不再执行任务执行时间 周期间隔时不会并发执行会延迟执行同样无界队列任务过多会 OOM单线程版本效率极低一个任务阻塞全部卡住一句话总结定时任务专用但异常会中断调度 无界队列风险线程池核心特点最大坑点Fixed固定线程数无界队列任务堆积 → OOMCached无限线程不排队线程爆炸 → OOMSingle单线程串行效率低 堆积 OOMScheduled定时 / 周期执行异常中断调度 延迟执行6.为什么阿里禁止用 Executors 创建线程池直接使用Executor创建线程池会导致队列、线程池最大容量没有设计上限在高并发场景下会耗尽服务器资源直接引发内存溢出OOM。考虑到两个OOM情况线程爆炸在executor创建Cached线程池的时候会因无限容器大小且高并发的情况下创建过多线程导致耗空系统资源OOM的情况拒绝策略永远不会触发最大线程数形同虚设任务堆积在创建executor创建Fixed或Scheduled的时候会因为有固定大小的容量且无限大小的队列(LinkedBlockingQueue/DelayedWorkQueue)会导致消息堆积导致OOM并且周期任务抛异常会直接停止调度无容错无法自定义线程名Executors创建的线程名默认是pool-1-thread-1出问题无法快速定位业务代码排查困难7.核心线程数怎么合理设置IO 密集型、CPU 密集型公式线程池线程数要根据任务类型设置分为 CPU 密集型 和 IO 密集型。CPU密集型公式corePoolSize CPU核数 1特点纯计算无 IO 等待目的减少线程上下文切换让 CPU 跑满为什么 1防止线程偶尔阻塞仍能榨干 CPUIO密集型公式corePoolSize CPU核数 * 2特点大量等待 IOCPU 空闲时间多目的CPU 等待时用其他线程继续干活生产最常用、最安全CPU × 2场景一IO密集型电商场景特点瞬时高并发、任务处理时间短线程池的配置可设置如下new ThreadPoolExecutor( 16, // corePoolSize 16假设8核CPU × 2 32, // maximumPoolSize 32突发流量扩容 10, TimeUnit.SECONDS, // 非核心线程空闲10秒回收 new SynchronousQueue(), // 不缓存任务, 直接扩容线程 new AbortPolicy() // 直接拒绝, 避免系统过载 );说明使用SynchronousQueue确保任务直达线程避免队列延迟。拒绝策略快速失败前端返回“活动火爆”提示结合降级策略如缓存预热。场景二CPU 密集型场景纯计算任务场景描述视频帧处理、图片滤镜、加密解密、大数据排序、复杂公式计算无 IO、无等待、纯吃 CPU线程池配置8 核 CPUnew ThreadPoolExecutor( 9, // corePoolSize CPU 核心数 1 → 819 9, // maximumPoolSize 和核心线程一样固定线程 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(100), // 有界队列少量排队 new ThreadPoolExecutor.CallerRunsPolicy() // 过载让调用者执行不丢任务 );8.线程池空闲线程回收机制总结当一个线程空闲时间超过 keepAliveTime并且当前线程数 核心线程数或允许核心线程超时就会被回收只在特定情况回收核心线程。主要由两个参数keepAliveTime和allowCoreThreadTimeOut和一个方法getTask()控制参数作用默认值keepAliveTime线程的最大空闲时间超过这个时间就会被回收无allowCoreThreadTimeOut是否允许核心线程被回收false默认不回收核心线程while (true) { // 1. 判断是否需要超时等待 boolean timed allowCoreThreadTimeOut || wc corePoolSize; // 2. 从队列取任务 // - timedtrue超时等待 keepAliveTime 时间 // - timedfalse无限阻塞等待核心线程默认 Runnable task timed ? workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take(); // 3. 超时没拿到任务 → 返回 null → 线程被回收 if (task ! null) return task; }可知getTask()是获取任务的方法如果在超时的情况下没有拿到线程则会被回收线程工作线程的生命周期线程被创建后进入循环不断调用getTask()从队列取任务拿到任务 → 执行run()方法执行完任务 → 回到循环再次调用getTask()如果getTask()返回null→ 线程退出被回收线程池核心线程数最大线程数keepAliveTime回收特性FixedThreadPoolnn0s无回收coremax没有非核心线程CachedThreadPool0Integer.MAX_VALUE60s所有线程空闲 60 秒自动回收SingleThreadExecutor110s无回收只有一个核心线程ScheduledThreadPoolnInteger.MAX_VALUE0s只回收非核心线程核心线程永久驻留只有Cached线程池由默认值回收时间60s9.线程池关闭 shutdown () 和 shutdownNow () 区别对比维度shutdown()shutdownNow()线程池状态变为 SHUTDOWN变为 STOP已提交正在执行的任务继续执行直到完成尝试中断仅设置中断标志位队列中等待的任务全部执行全部丢弃不执行返回值无void返回队列中未执行的任务列表 ListRunnable能否提交新任务❌ 不能抛出 RejectedExecutionException❌ 不能抛出 RejectedExecutionException中断对象仅中断空闲线程中断所有线程包括正在执行的关闭速度慢等待所有任务完成快立即停止大部分任务任务丢失无丢失队列中所有等待的任务shutdown()和shutdownNow()是线程池关闭的两个核心方法主要区别在于对任务的处理方式。shutdown()是温柔关闭设置状态为 SHUTDOWN中断空闲线程等待所有已提交任务包括队列中的执行完成后关闭不丢失任务。shutdownNow()是暴力关闭设置状态为 STOP中断所有线程清空队列并返回未执行的任务列表会放弃执行队列中的未执行任务。最重要的一点shutdownNow()只是设置中断标志位如果任务不响应中断线程会继续运行。生产环境推荐使用优雅关闭流程先调用shutdown()等待超时再调用shutdownNow()。10.线程池任务提交 execute () 和 submit () 区别execute()只能提交Runnable任务无返回值异常直接抛出submit()可以提交Runnable和Callable任务有返回值 Future异常会被捕获只有调用get()时才抛出对比维度execute()submit()方法所属接口Executor 接口ExecutorService 接口继承自 Executor支持的任务类型仅支持 Runnable支持 Runnable 和 CallableT返回值无void返回 FutureT 对象可获取任务执行结果异常处理异常直接抛出到控制台主线程无法捕获异常被封装在 Future 中只有调用 Future.get() 时才会抛出底层实现线程池核心提交方法底层调用 execute()只是把任务包装成 FutureTask使用场景不需要返回结果的简单异步任务需要获取执行结果、需要捕获异常的任务execute()和submit()都是线程池提交任务的方法主要区别在于返回值和异常处理。execute()只能提交Runnable任务没有返回值任务异常会直接抛出主线程无法捕获。submit()可以提交Runnable和Callable任务返回Future对象可以获取任务执行结果任务异常会被封装在 Future 中只有调用get()时才会抛出。submit()底层其实是调用execute()只是把任务包装成了FutureTask。注意如果调用submit()后不调用get()任务的异常会被完全吞掉导致问题难以排查。11.线程池异常怎么捕获线程池异常捕获的根本难点任务是在独立的工作线程中执行的异常无法直接抛回主线程。不同的提交方式execute()/submit()异常的传播路径完全不同。方案 1submit()Future.get()捕获最常用适用场景使用submit()提交任务需要获取返回值或明确知道任务执行结果。原理submit()提交时会把任务封装成FutureTask,任务执行过程中抛出的任何异常都会被捕获并保存到FutureTask的outcome字段中。只有调用get()方法时才会把异常包装成ExecutionException抛出FutureInteger future executor.submit(() - { // 可能抛出异常的任务 return 1 / 0; }); try { Integer result future.get(); // 这里才会抛出异常 } catch (ExecutionException e) { // 捕获任务抛出的异常 Throwable cause e.getCause(); // 获取原始异常 log.error(任务执行失败, cause); } catch (InterruptedException e) { // 捕获等待过程中被中断的异常 Thread.currentThread().interrupt(); log.error(等待任务结果被中断, e); }注意如果调用submit()后不调用get()异常会被完全吞掉 没有任何日志没有任何提示你永远不知道任务执行失败了。方案 2自定义UncaughtExceptionHandler捕获execute()异常适用场景使用execute()提交任务不需要返回值。原理execute()提交的任务抛出异常时不会被线程池捕获会直接向上抛到线程的UncaughtExceptionHandler。如果没有自定义处理器默认会打印到控制台。注意主线程的try-catch永远捕获不到execute()的异常// 错误写法永远捕获不到 try { executor.execute(() - { throw new RuntimeException(任务异常); }); } catch (Exception e) { // 这里永远不会执行 log.error(捕获到异常, e); }正确做法自定义线程工厂设置全局异常处理器// 自定义线程工厂给每个线程设置异常处理器 ThreadFactory threadFactory r - { Thread thread new Thread(r); thread.setName(my-thread-pool-%d); // 设置未捕获异常处理器 thread.setUncaughtExceptionHandler((t, e) - { log.error(线程 {} 发生未捕获异常, t.getName(), e); }); return thread; }; // 创建线程池时使用自定义线程工厂 ExecutorService executor new ThreadPoolExecutor( 8, 16, 60, TimeUnit.SECONDS, new ArrayBlockingQueue(100), threadFactory, // 关键 new AbortPolicy() );方案 3重写ThreadPoolExecutor.afterExecute()方法最全面适用场景生产环境推荐同时捕获execute()和submit()的所有异常包括submit()不调用get()的情况。原理ThreadPoolExecutor提供了钩子方法afterExecute(Runnable r, Throwable t)每个任务执行完成后都会调用这个方法。如果是execute()提交的任务异常会直接通过t参数传入如果是submit()提交的任务t参数为null需要从Future中取出异常方案 4CompletableFuture异常处理现代 Java 推荐适用场景使用CompletableFuture进行异步编程Java 8。原理CompletableFuture提供了专门的异常处理方法exceptionally()和handle()比传统的Future.get()更优雅。CompletableFuture.supplyAsync(() - { // 任务逻辑 return 1 / 0; }, executor) .exceptionally(e - { // 捕获异常返回默认值 log.error(任务执行失败, e); return 0; }) .thenAccept(result - { // 处理正常结果 System.out.println(结果 result); });捕获方案适用提交方式优点缺点submit() Future.get()仅 submit()简单直接能获取返回值不调用 get() 异常被吞UncaughtExceptionHandler仅 execute()全局统一处理 execute() 异常无法处理 submit() 异常重写 afterExecute()execute() submit()最全面所有异常都能捕获需要自定义线程池CompletableFuture 异常处理CompletableFuture链式调用优雅灵活仅适用于 CompletableFuture面试回答直接背线程池异常捕获主要有 4 种方案不同提交方式的异常传播路径不同。对于submit()提交的任务异常会被封装在Future对象中只有调用get()方法时才会抛出ExecutionException如果不调用get()异常会被完全吞掉。对于execute()提交的任务异常会直接抛到线程的UncaughtExceptionHandler主线程无法捕获需要自定义线程工厂设置全局异常 处理器。最全面的方案是重写ThreadPoolExecutor的afterExecute()钩子方法它可以同时捕获execute()和submit()的所有异常包括submit()不调用get()的情况。Java 8 还可以使用CompletableFuture的exceptionally()方法进行更优雅的异常处理。