线程池ThreadPoolExecutor 面试看这一篇就够了(全考点覆盖)

面试知识点

阻塞队列的类型,如何选择?

有界队列

  • 队列满时会执行拒绝策略,防止任务无限堆积导致的oom
  • 适用于需要保证稳定性资源有限任务处理速度小于生产速度的场景

无界队列

  • 任务可以无限堆积直至内存耗尽
  • 适用于任务量可控,且任务处理速度远大于生成速度的场景

此外还有一种特殊的阻塞队列SynchronousQueue,被newCachedThreadPool所采用,没有容量,只有有线程来承接这个任务才会接收下一个任务,一手交钱一手交货

cpu密集型/io密集型如何配置线程池参数

CPU密集型任务 IO密集型任务
特点 任务主要消耗CPU资源;线程大部分时间在执行计算,而非等待。 任务主要时间花在等待IO操作(如网络请求、数据库查询、文件读写);CPU经常空闲,线程可切换去执行其他任务。
线程数量配置公式 线程数 = CPU核心数 + 1 线程数 = CPU核心数 * 目标CPU利用率 * (1 + 平均IO等待时间 / 平均CPU计算时间) (简化版:线程数 = 2 * CPU核心数
原理 CPU核心数是最大并行计算能力上限;多1个线程是为了在某个线程因操作系统或任务调度短暂停顿时,利用空闲的CPU资源。 IO等待时间越长,可创建的线程越多,以充分利用CPU空闲时间
注意事项 避免过多线程导致频繁的线程上下文切换,降低性能。

尽量避免在使用线程池时操作ThreadLocal

工作线程的生命周期通常都会超过任务的生命周期

如果任务结束时未清空 ThreadLocal 会导致下一个任务复用线程时读取到上一个任务 存储在线程ThreadLocal 中的脏信息

ThreadPoolExecutor

1.状态

状态名 高 3 位 接收新任务 处理阻塞队列任务 说明
RUNNING 111 Y Y
SHUTDOWN 000 N Y 不会接收新任务,但会处理阻塞队列剩余任务
STOP 001 N N 会中断正在执行的任务,并抛弃阻塞队列任务
TIDYING 010 - - 任务全执行完毕,活动线程为 0 即将进入终结
TERMINATED 011 - - 终结状态

ThreadPoolExecutor 使用 int 的高 3 位来表示线程池状态,低 29 位表示线程数量

这些信息存储在一个原子变量 ctl 中,目的是将线程池状态与线程个数合二为一,这样就可以用一次 cas 原子操作进行赋值

// c 为旧值,ctl0f 返回结果为新值
ctl.compareAndSet(c, ctl0f(targetState, workerCountOf(c)));

// rs 为高 3 位代表线程池状态,wc 为低 29 位代表线程个数,ctl 是合并它们
private static int ctlOf(int rs, int wc) { return rs | wc; }

2.参数

public ThreadPoolExecutor(int corePoolSize,
                         int maximumPoolSize,
                         long keepAliveTime,
                         TimeUnit unit,
                         BlockingQueue<Runnable> workQueue,
                         ThreadFactory threadFactory,
                         RejectedExecutionHandler handler)
  • corePoolSize 核心线程数目(最多保留的线程数)
  • maximumPoolSize 最大线程数目
  • keepAliveTime 生存时间 - 针对救急线程
  • unit 时间单位 - 针对救急线程
  • workQueue 阻塞队列
  • handler 拒绝策略
  • threadFactory 线程工厂 - 可以为线程创建时起个好名字

jdk的线程池ThreadPoolExecutor采用了救急线程机制

  • 当线程池中的核心线程都在执行且任务队列(前提采用有界队列)已满时,会创建救急线程来执行任务,执行完毕后根据参数配置的销毁时间进行回收
  • 只有当核心线程,阻塞队列已满且救急线程也都在运行时才会执行拒绝策略

3.拒绝策略

  • AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略

  • CallerRunsPolicy 让调用者运行任务

  • DiscardPolicy 放弃本次任务

  • DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之

  • Dubbo 的实现,在抛出 RejectedExecutionException 异常之前会记录日志,并 dump 线程栈信息,方便定位问题

  • Tomcat :不会立马抛出异常而是再尝试一次将任务放入阻塞队列,如果还是失败才执行拒绝策略; 类似于自旋锁,多了一次自旋操作

    --> 高并发场景的容错优化: Web请求常出现突发流量,线程池可能瞬间满载。此时直接拒绝会导致大量请求失败,短暂重试可消化部分因线程调度延迟或锁竞争导致的"假性满载",避免误杀请求

4.常用线程池工厂

newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

特点

  • 核心线程数 == 最大线程数(没有救急线程被创建),因此也无需超时时间
  • 阻塞队列是无界的,可以放任意数量的任务(需要避免任务堆积,防止阻塞队列积攒任务导致 OOM)

评价 适用于任务量已知,相对耗时的任务

newCachedThreadPool

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

特点

  • 核心线程数是 0,最大线程数是 Integer.MAX_VALUE,救急线程的空闲生存时间是 60s,意味着全部都是救急线程(60s 后可以回收)
  • 救急线程可以无限创建
  • 队列采用了 SynchronousQueue 实现特点是,它没有容量,没有线程来取是放不进去的(一手交钱、一手交货)

评价 整个线程池表现为线程数会根据任务量不断增长,没有上限,当任务执行完毕,空闲 1 分钟后释放线程。 适合处理大量短时任务(任务数比较密集,但每个任务执行时间较短)的情况

newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
    (new ThreadPoolExecutor(1, 1,
                          0L, TimeUnit.MILLISECONDS,
                          new LinkedBlockingQueue<Runnable>()));
}

使用场景: 希望多个任务排队执行。线程数固定为 1,任务数多于 1 时,会放入无界队列排队。任务执行完毕,这唯一的线程也不会被释放。

区别:

  • 自己创建一个单线程串行执行任务,如果任务执行失败而终止那么没有任何补救措施,而线程池还会新建一个线程,保证池的正常工作
  • Executors.newSingleThreadExecutor() 线程个数始终为 1,不能修改
    • FinalizableDelegatedExecutorService 应用的是装饰器模式,只对外暴露了 ExecutorService 接口,因此不能调用 ThreadPoolExecutor 中特有的方法
  • Executors.newFixedThreadPool(1) 初始时为 1,以后还可以修改
    • 对外暴露的是 ThreadPoolExecutor 对象,可以强转后调用 setCorePoolSize 等方法进行强行修改

装饰器模式:没有新增逻辑,只是间接调用方法,起到一个对外方法过滤的作用,以实现外部无法调用核心特有方法来保证安全性

例子:

  1. 线程池工具类:newSingleThreadExecutor 单例线程池
  2. 线程安全集合类中的修饰安全集合类
    • 传入map,返回一个线程安全map,其实内部还是调用原map的方法,只不过每次调用前都多加了一个synchronized

5.提交任务

// 执行任务
void execute(Runnable command);

// 提交任务 task,用返回值 Future 获得任务执行结果
<T> Future<T> submit(Callable<T> task);

// 提交 tasks 中所有任务
// 适用场景:批量提交任务并等待所有任务完成,收集全部结果。
// 示例: 并行处理多个独立计算(如批量调用API),汇总结果。
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
    throws InterruptedException;

// 提交 tasks 中所有任务,哪个任务先成功执行完毕,返回此任务执行结果,其它任务取消
// 适用场景:多个任务解决同一问题,取首个成功结果并取消其余任务。
// 示例:冗余服务调用(如多个镜像源下载,取最快响应)。
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
    throws InterruptedException, ExecutionException;

异常捕获

线程池submit()执行中的异常默认不会被处理,有以下两种处理方案

  1. 代码中通过try{}catch{}主动处理异常
    • 捕获异常后log打印日志记录
  2. Future+Callable的形式接收执行结果
    • future的get()如果正常执行则获取到返回值
    • 如果执行中出现异常get()获取到到的就是异常信息
#java#
fengdongnan的博客 文章被收录于专栏

记录fengdongnan的知识产出文档,欢迎大家来一起交流学习

全部评论

相关推荐

09-17 17:09
门头沟学院 Java
雨忄:有人给出过解法,拖晚点去,然后到时候再找其他理由商量,既增加他们的筛人成本,不一定会给你收回offer ,也能占位避免工贼
秋招的嫡长offer
点赞 评论 收藏
分享
09-18 18:25
已编辑
复旦大学 C++
找工小学弟:还得是旦✌️
投递淘天集团等公司10个岗位
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务