目录
3.3.1 JDK提供的四种拒绝策略(RejectedExecutionHandler)
3.4 (重点)ThreadPoolExecutor 源码分析
一、什么是线程池
1.1、什么是线程池
线程池的核心思想是“线程复用”,通过维护一个线程集合(池)和任务队列,将任务提交与线程调度解耦。
核心组件
1、线程集合:预先创建核心线程与非核心线程
核心线程:线程可以长期保留。
非核心线程:设置一个时间,当没有任务需要线程处理时,超时后线程就会被销毁。
2、任务队列:存储待执行的任务(一般使用阻塞队列),避免任务堆积时直接创建过多的线程。
3、拒绝策略:当任务超出处理能力时,定义如何处理新任务(丢弃、抛出异常....)。
1.2、为什么用线程池
1、降低资源开销
- 线程创建/销毁成本高
- 操作系统创建线程涉及内存分配、上下文切换等操作,频繁操作会消耗大量的资源
- 线程池复用线程
- 通过复用已有的线程,减少系统开销,提升响应速度
2、控制并发规模
- 防止资源耗尽
- 通过限制最大线程数,避免无限制创建线程导致内存或CPU过载(”线程爆炸“问题)
- 任务队列缓冲
- 当任务激增时,队列作为缓冲区平增流量,避免系统瞬间崩溃
3、提升可管理性
- 统一监控与调优
- 可统计线程执行情况、调整核心参数(后面在ThreadPoolExecutor中会详细介绍的),优化系统功能
- 灵活的任务调度
- 支持定时任务、优先级任务等扩展功能(如ScheduleThreadPoolExecutor)
二、JDK自带的构建线程池方式
JDK中基于Executor提供了很多种线程池
2.1 newFixedThreadPool
这个线程池的特点是:线程数是固定的。
- 在构建时,需要给newFixedThreadPool方法提供一个nThread参数。这个参数就是当前线程池种的个数,当前线程池的不呢之其实就是使用ThreadPoolExecutor。
- 构建好当前线程池后,线程个数已经固定好(线程是懒加载,在构建之初,线程并没有构建出来,而是随着任务的提交才会将线程在线程池中构建出来)。如果线程没构建,线程会待着任务执行被创建和执行。如果线程都已经构建好了,此时任务会被放到LinkedBlockingQueue无界队列中存放,等待线程从LinkedBlockingQueue中去take出任务,然后执行。
2.2 newSingleThreadExecutor
这个线程池是单例线程池,线程池种只有一个工作线程在处理任务。
使用场景:如果涉及到顺序消费可以使用这个
- 在内部依然是构建了ThreadPoolExecutor,设置的线程个数为1
- 当任务投递过来后,第一个任务会被工作线程处理,后续的任务会被扔到阻塞队列中
- 投递到阻塞队列中任务的顺序,就是工作线程处理的顺序
- 当前这种线程池可以用作顺序处理的一些业务中
如果是局部变量仅限当前线程池使用的线程池,在使用完毕之后要记得执行shutdown,避免线程无法结束
2.3 newCachedThreadPool
这个线程池的最大线程数Integer的最大数。
1、当第一次提交任务到线程池时,会直接构建一个工作线程,
2、这个工作线程等任务执行完后,60秒米有任务可以执行,会结束
3、如果在等待期间(60S)内有任务来,他会再次拿到这个任务去执行
4、如果后续有任务提交,但是没有线程是空闲的,那么就会构建工作线程去执行。
最大特点:任务只要提交给当前的线程池,就必然会有工作线程可以处理
2.4 newScheduleThreadPool
定时任务的线程池,而这个线程池就是可以以一定周期去执行一个任务,或者是延迟多久执行一个任务一次
原理是基于DelayQueue实现的延迟执行。周期性执行是任务执行完毕后,再次扔回到阻塞队列。
2.5 newWordStealingPool
- 当前JDK提供构建线程池的方式newWorkStealingPool和之前的线程池很非常大的区别
- 之前定长,单例,缓存,定时任务都基于ThreadPoolExecutor去实现的。
- newWorkStealingPool是基于ForkJoinPool构建出来的
ThreadPoolExecutor的核心点:
在ThreadPoolExecutor中只有一个阻塞队列存放当前任务
ForkJoinPool的核心特点:
- 当有一个特别大的任务时,如果采用上述方式(ThreadPoolExecutor)这个大任务只会让某一个线程执行。
- ForkJoin第一个特点时可以将一个大任务拆分成多个小任务,放到当前线程的阻塞队列种,其他的空闲线程就可以去处理有任务的线程的阻塞队列种的任务。
举例:来一个比较大的数组,里面存满值,计算总和 :
单线程处理一个任务:
/** 非常大的数组 */ static int[] nums = new int[1_000_000_000]; // 填充值 static{ for (int i = 0; i < nums.length; i++) { nums[i] = (int) ((Math.random()) * 1000); } } public static void main(String[] args) { // ===================单线程累加10亿数据================================ System.out.println("单线程计算数组总和!"); long start = System.nanoTime(); int sum = 0; for (int num : nums) { sum += num; } long end = System.nanoTime(); System.out.println("单线程运算结果为:" + sum + ",计算时间为:" + (end - start)); }
多线程分而治之的方式处理
/** 非常大的数组 */ static int[] nums = new int[1_000_000_000]; // 填充值 static{ for (int i = 0; i < nums.length; i++) { nums[i] = (int) ((Math.random()) * 1000); } } public static void main(String[] args) { // ===================单线程累加10亿数据================================ System.out.println("单线程计算数组总和!"); long start = System.nanoTime(); int sum = 0; for (int num : nums) { sum += num; } long end = System.nanoTime(); System.out.println("单线程运算结果为:" + sum + ",计算时间为:" + (end - start)); // ===================多线程分而治之累加10亿数据================================ // 在使用forkJoinPool时,不推荐使用Runnable和Callable // 可以使用提供的另外两种任务的描述方式 // Runnable(没有返回结果) -> RecursiveAction // Callable(有返回结果) -> RecursiveTask ForkJoinPool forkJoinPool = (ForkJoinPool) Executors.newWorkStealingPool(); System.out.println("分而治之计算数组总和!"); long forkJoinStart = System.nanoTime(); ForkJoinTask<Integer> task = forkJoinPool.submit(new SumRecursiveTask(0, nums.length - 1)); Integer result = task.join(); long forkJoinEnd = System.nanoTime(); System.out.println("分而治之运算结果为:" + result + ",计算时间为:" + (forkJoinEnd - forkJoinStart)); } private static class SumRecursiveTask extends RecursiveTask<Integer>{ /** 指定一个线程处理哪个位置的数据 */ private int start,end; private final int MAX_STRIDE = 100_000_000; // 200_000_000: 147964900 // 100_000_000: 145942100 public SumRecursiveTask(int start, int end) { this.start = start; this.end = end; } @Override protected Integer compute() { // 在这个方法中,需要设置好任务拆分的逻辑以及聚合的逻辑 int sum = 0; int stride = end - start; if(stride <= MAX_STRIDE){ // 可以处理任务 for (int i = start; i <= end; i++) { sum += nums[i]; } }else{ // 将任务拆分,分而治之。 int middle = (start + end) / 2; // 声明为2个任务 SumRecursiveTask left = new SumRecursiveTask(start, middle); SumRecursiveTask right = new SumRecursiveTask(middle + 1, end); // 分别执行两个任务 left.fork(); right.fork(); // 等待结果,并且获取sum sum = left.join() + right.join(); } return sum; } }
- 最终可以发现,这种累加的操作中,采用分而治之的方式效率提升了2倍多。
- 但是也不是所有任务都能拆分提升效率,首先任务得大,耗时要长。
三、(重点)ThreadPoolExecutor
3.1 为什么要自定义线程池
1、首先ThreadPoolExecutor中,一共提供了7个参数,每个参数都是非常核心的属性,在线程池去执行任务时,每个参数都起决定性的作用。
2、但是如果直接采用JDK提供的方式去构建,可以设置的核心参数最多就两个,这样就会导致对线程池的控制粒度很粗。所以在阿里规范中也推荐自己去自定义线程池。手动的去new ThreadPoolExecutor设置他的一些核心属性。
3、自定义构建线程池,可以细粒度的控制线程池,去管理内存的属性,并且针对一些参数的设置可能更好的在后期排查问题。
3.2 ThreadPoolExecutor中的7个参数
public ThreadPoolExecutor(
int corePoolSize, // 核心工作线程(当前任务执行结束后,不会被销毁)
int maximumPoolSize, // 最大工作线程(代表当前线程池中,一共可以有多少个工作线程)
long keepAliveTime, // 非核心工作线程在阻塞队列位置等待的时间
TimeUnit unit, // 非核心工作线程在阻塞队列位置等待时间的单位
BlockingQueue<Runnable> workQueue, // 任务在没有核心工作线程处理时,任务先扔到阻塞队列中
ThreadFactory threadFactory, // 构建线程的线程工作,可以设置thread的一些信息
RejectedExecutionHandler handler) { // 当线程池无法处理投递过来的任务时,执行当前的拒绝策略
// 初始化线程池的操作
}
当核心线程满了,任务会扔到阻塞队列,当阻塞队列满了,这是再来任务,才会尝试创建非核心线程
如果当前正在运行的核心线程数小于corePoolSize,线程池会优先创建新的核心线程来执行任务,而不是直接将任务放入队列或创建非核心线程。
3.3 ThreadPoolExecutor应用
3.3.1 JDK提供的四种拒绝策略(RejectedExecutionHandler
)
3.3.1.1 AbortPolicy(默认)
- 当前拒绝策略会在无法处理任务时,直接抛出一个异常
RejectedExecutionException
- 使用场景:需要严格保证任务不丢失的场景,开发者需要显示捕获异常并处理
3.3.1.2 CallerRunsPolicy
- 行为:由提交任务也的线程(调用者线程)直接执行被拒绝的任务
- 使用场景:希望降低任务提交速度,同时保证任务不丢失。
- eg. 任务提交高峰期间,调用者线程直接执行任务,减轻线程池压力。
- 注意:若提交线程(调用者线程)是main线程,可能阻塞main线程;适用于异步性要求不高的场景
3.3.1.3 DiscardPolicy
- 行为:静默丢弃被拒绝的任务,不抛出异常,也不执行任何操作。
- 使用场景:允许任务丢失的非关键场景。eg. 日志记录、监控数据...。
- 注意事项:对业务任务丢失有一定的容忍度,否则会导致数据不一致。
3.3.1.4 DIscardOldestPolicy
- 行为:丢弃队列中最旧(队列排第一位)的任务,然后尝试重新提交当前任务。
- 使用场景:队列中的旧任务优先级低,允许用新任务替代。
- 注意事项:若队列为优先级队列(如
PriorityBlockingQueue
),可能丢弃高优先级任务,需谨慎使用。
队列类型 数据结构 容量 锁机制 特点 PriorityBlockingQueue 堆(数组实现) 无界 单锁 元素按优先级排序,支持自定义比较器。
3.3.1.5 如何设置(使用)拒绝策略
在创建线程池时,通过构造函数指定拒绝策略。
3.3.1.6 选择拒绝策略的一些建议
-
严格任务保障:使用
AbortPolicy
并结合异常处理逻辑。 -
流量削峰:使用
CallerRunsPolicy
,利用调用者线程分担压力。 -
容忍任务丢失:选择
DiscardPolicy
或DiscardOldestPolicy
,但需评估业务影响。 -
自定义策略:实现
RejectedExecutionHandler
接口,根据业务需求定制逻辑(如记录日志、重试等)。 -
当线程池已关闭(
shutdown
)时,任何提交的任务都会触发拒绝策略。
3.4 (重点)ThreadPoolExecutor 源码分析
由于篇幅过长点击链接跳转 -->ThreadPoolExecutor 源码分析
四、线程池的核心参数设计规则
线程池的使用难度不大,难度在于线程池的参数并不好配置。
主要难点在于任务类型无法控制,比如任务有CPU密集型,还有IO密集型,甚至还有混合型的。
因为IO咱们无法直接控制,所以很多时间按照一些书上提供的一些方法,是无法解决问题的。
《Java并发编程实践》
想调试出一个符合当前任务情况的核心参数,最好的方式就是测试。
需要将项目部署到测试环境或者是沙箱环境中,结果各种压测得到一个相对符合的参数。
如果每次修改项目都需要重新部署,成本太高了。
此时咱们可以实现一个动态监控以及修改线程池的方案。
因为线程池的核心参数无非就是:
-
corePoolSize:核心线程数
-
maximumPoolSize:最大线程数
-
workQueue:工作队列
线程池中提供了获取核心信息的get方法,同时也提供了动态修改核心属性的set方法。
也可以采用一些开源项目提供的方式去做监控和修改
比如hippo4j就可以对线程池进行监控,而且可以和SpringBoot整合。
官方文档:https://hippo4j.cn/docs/user_docs/intro
五、线程池处理任务的核心流程
基于addWorker添加工作线程的流程切入到整体处理任务的位置