前言
由于线程池相关核心内容较长,为了方便大家阅读,会将内容分为上下两篇,上篇主要讲解如何使用,以及线程池的基本原理;下篇深入源码展开分析验证原理的正确性。
一、线程池的创建与使用?
方案1:使用Executors工具类创建 (强烈不推荐)
// 初始化一个核心线程数为0、最大线程数为Integer.MAX_VALUE,队列为SynchronousQueue,线程空闲时间为60s的线程池
ExecutorService executorService = Executors.newCachedThreadPool();
存在的问题:
SynchronousQueue是一个无缓冲的阻塞队列,所以提交任务后,必须有线程去执行,否则会一直阻塞住
线程池的线程数量会根据任务数量动态调整,如果有大量的任务,可能会导致CPU飙升,甚至OOM
线程池的线程数量会根据任务数量动态调整,所以任务执行完成后,线程池中的线程数量会一直减少,最终为0,所以线程池中的线程数量会一直变化
// 初始化一个核心线程数为1、最大线程数为1,队列为LinkedBlockingQueue,线程空闲时间为0s的线程池
ExecutorService executorService1 = Executors.newFixedThreadPool(1);
存在的问题:
LinkedBlockingQueue是一个有界队列,所以提交任务后,不会阻塞,但是会一直往队列中提交任务,直到队列元素达到Integer.MAX_VALUE,导致OOM
// 初始化一个核心线程数为1、最大线程数为1,队列为LinkedBlockingQueue,线程空闲时间为0s的线程池
ExecutorService executorService2 = Executors.newSingleThreadExecutor();
存在的问题:
LinkedBlockingQueue是一个有界队列,所以提交任务后,不会阻塞,但是会一直往队列中提交任务,直到队列元素达到Integer.MAX_VALUE,导致OOM
/// 初始化一个核心线程数为1、最大线程数为Integer.MAX_VALUE,队列为DelayQueue,线程空闲时间为0s的线程池
ScheduledExecutorService executorService3 = Executors.newScheduledThreadPool(1);
存在的问题:
DelayQueue是无界的,这意味着它可以无限地添加元素,直到系统内存耗尽。
最大线程数为Integer.MAX_VALUE,这意味着线程池可以无限地创建线程,直到系统内存耗尽。
方案2:自定义线程池 不太建议
// 初始化一个核心线程数为1、最大线程数为10,队列为ArrayBlockingQueue,线程空闲时间为60s的线程池
ThreadPoolExecutor poolExecutor =
new ThreadPoolExecutor(1, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(500));
存在的问题:
线程参数无法正确设置,业界是否存在一劳永逸的办法?很遗憾:答案是没有;以下截图来源于美团线程池分析文章
线程池在生产环境中运行过程中的状态无法可视化监控,出现问题无法快速感知
系统发出异常告警时,无法快速调整线程池参数(当然ThreadPoolExecutor本身提供口子用于动态修改相关参数,但需要开发者开发一整套工具来方便调整参数)
方案3:开源的动态线程池 推荐
当公司内没有能力或资源专门开发线程池组件时,开源的Java线程池组件,其基本思路是基于动态线程池去实现的,这些开源的线程池也有很多公司在使用,同时版本迭代快,社区活跃度也比较高。
DynamicTp,具备动态调参、通知报警、运行监控等功能,可以解决核心参数无法一步设置到合理值的窘境。官网地址: https://dynamictp.cn/ , Github: https://github.com/dromara/dynamic-tp
Hippo4j,具备动态变更、自定义报警、运行监控等功能,可以解决核心参数无法一步设置到合理值的窘境。官网地址: https://hippo4j.cn/zh/ , Github: https://github.com/opengoofy/hippo4j
Spring Boot Actuator,Spring内置的一个监控功能,严格意义上不太算线程池组件。官网地址:https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html
方案4:基于动态线程池自研的线程池组件 强烈推荐
公司架构团队专门针对自身业务场景而研发的动态线程池,比如在动态线程池的基础上,开发了亲缘性线程池等。
小结:简单分析后可以看出,实际生产环境中线程池使用优先级应该是:公司自研线程池组件 > 三方开源线程池组件 > 自定义线程池 > 坚决不使用JDK自带的线程池组件。
二、线程池的基本工作原理
到此,我们已经掌握了如何创建和使用线程池了,下面我们来看看线程池是怎么运转的。整个线程池其实可以看做是一个生产者/消费者模型,任务源源不断生产出来提交到线程池,阻塞队列作为缓冲区暂存任务,线程池内的线程则不断从缓冲区获取任务进行消费;通过池化的思想来避免线程重复重建、销毁带来的性能损耗。
首先初始化一个线程池,注意此时仅创建了一个线程池空壳子,并没有创建出来任何线程
当有任务提交到线程池执行,若活跃的线程数小于核心线程数,则创建一个新的线程来处理提交上来的任务
若活跃的线程数大于等于核心线程数,则将提交的任务暂存到阻塞/非阻塞队列中,等候执行
当任务还在源源不断的提交上来,直到塞满队列后,此时若活跃线程数小于最大核心线程数,则继续创建新的线程来处理新提交上来的任务
若当前活跃线程数超过最大核心数后,新提交上来的任务就需要执行相应的拒绝策略了。i)
CallerRunsPolicy
:检查当前线程池状态若还在正常运行时,则直接使用提交任务的线程去执行该任务 ii)AbortPolicy
: 将新提交到线程池的任务直接丢弃掉,并抛出RejectedExecutionException
异常,交由业务代码进行处理 iii)DiscardPolicy
: 将新提交到线程池的任务直接丢弃掉,但不会抛出异常让外界感知,一般线上业务不会也不允许使用该策略 iv)DiscardOldestPolicy
: 首先将队列中队头任务丢弃掉(不抛出任何异常),然后将当前任务加入到阻塞队列中
后续
本篇文章主要讲解了线程池如何创建和使用,以及从理论层面初步分析了线程池的基本工作过程,下篇文章我们开始进一步深入到源码来对理论进行验证。