线程池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 等方法进行强行修改
装饰器模式:没有新增逻辑,只是间接调用方法,起到一个对外方法过滤的作用,以实现外部无法调用核心特有方法来保证安全性
例子:
- 线程池工具类:newSingleThreadExecutor 单例线程池
- 线程安全集合类中的修饰安全集合类
传入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()执行中的异常默认不会被处理,有以下两种处理方案
- 代码中通过try{}catch{}主动处理异常
- 捕获异常后log打印日志记录
- Future+Callable的形式接收执行结果
- future的get()如果正常执行则获取到返回值
- 如果执行中出现异常get()获取到到的就是异常信息
记录fengdongnan的知识产出文档,欢迎大家来一起交流学习