文章目录
前言
我们看到JDK内部,似乎是有为了我们使用线程池的方便性或减少我们线程池学习创建成本,内置了一个Executors类,类中提供了部分静态方法,让我们能够快速的创建并使用各种类型的线程池
那么,其中的这些方法是否可以我们搬来即用呢?
我这里先浅抛一个结论:通常情况下不建议直接使用Executors内置的方法去创建线程池(直白一点就是不建议使用),而是我们手动创建,那为什么呢?其方法是有什么隐患吗?请仔细看下文剖析
Executors内部结构剖析
Executors类是java语言 juc包下的一个类,内部提供了很多静态方法可以让我们快速的创建线程池,以及部分转换方法,比如Runable转换为Callable,因此Executors就是一个工具类。
接来来,我将给出不建议使用其创建线程池的理由
newFixedThreadPool
其目的是创建一个固定线程池;固定什么呢?固定线程数
相关方法
线程数
ExecutorService newFixedThreadPool(int nThreads)
线程数,线程工厂
ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
我们可以看到,两个方法为重载方法,一个只有一个线程数参数;一个既有线程数参数还有线程工厂参数
线程工厂-ThreadFactory的作用
ThreadFactory的作用就是可以修饰我们线程池中的线程
常用操作有 设置线程的名字,设置线程是否为守护线程等等
内部实现
创建一个固定线程数(参数传入)以及可指定线程工厂的,固定线程数的线程池;但本质是对底层线程池ThreadPoolExecutor
的包装,固定写死了部分参数 比如内部队列为new LinkedBlockingQueue<Runnable>()
、非核心线程数的存活时间以及存活参数,但因为核心线程与最大线程数设置一致,因此存活时间与存活时间单位这个两个参数毫无意义。
运行流程
1、主线程将请求丢给线程池
2、线程池中如果核心线程未完全创建完毕,会创建一个核心线程并立即执行当前请求
3、如果核心线程创建完毕且当前无线程空闲,这会将请求丢入队列
4、线程处理完请求后会不断的尝试从队列拉取数据进行消费
问题剖析
1、无法基于请求并发量的变化实现线程的扩容与消减,永远都是指定线程数
2、使用了LinkedBlockingQueue
无界队列,且未传入队列长度参数,故此其队列内部可支持的元素个数为Integer.MAX_VALUE
可视作为无界队列
致命问题就是因使用了无界队列,如果请求数据过多,且不会触发拒绝策略,如线程消费不过来久而久之会撑爆内存
newSingleThreadExecutor
创建一个只有一个线程的线程池
相关方法
ExecutorService newSingleThreadExecutor()
无参数
ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)
有且仅有一个参数,线程工厂
内部实现
创建一个可选线程工厂的,固定线程数大小为1的线程池;但本质是对底层线程池ThreadPoolExecutor
的包装,固定写死了部分参数 比如内部队列为new LinkedBlockingQueue<Runnable>()
、核心线程与最大线程都为1,所以时间参数也无意义。
运行流程
运行流程与newFixedThreadPool一致,唯一不同的是核心线程就只有一个,因此只有第一个任务来了后会创建线程池立即执行,其余的都是进入了队列后靠这唯一的线程不断尝试从队列获取并消费
问题剖析
1、无法基于请求并发量的变化实现线程的扩容与消减,永远都是指定线程数,且线程数为1
2、使用了LinkedBlockingQueue
,且未传入队列长度参数,故此其队列内部可支持的元素个数为Integer.MAX_VALUE
可视作为无界队列
缺点与newFixedThreadPool一致 >>>因使用了无界队列,如果请求数据过多,且不会触发拒绝策略,如线程消费不过来久而久之会撑爆内存
但它对比newFixedThreadPool有个优点,就是因为其线程数为1且固定,因此进入该线程池的请求处理都是有序的
newCachedThreadPool
一个可缓存的线程池(缓存啥呢?缓存线程?为啥叫缓存?线程在一定条件下可以快速拿出使用且量足够多)
相关方法
无参数
ExecutorService newCachedThreadPool()
有且仅有一个参数,线程工厂
ExecutorService newCachedThreadPool(ThreadFactory threadFactory)
内部实现
创建一个核心线程为0,最大线程为Integer.MAX_VALUE
(2147483647 21亿4千万)的线程池 ;但本质是对底层线程池ThreadPoolExecutor
的包装,线程存活时间是60s,且使用了同步队列new SynchronousQueue<Runnable>()
SynchronousQueue
SynchronousQueue 就是同步队列,其内部不会存储元素,当一个线程调用了put方法时,如果队列中没有take线程,那么put线程就会阻塞,当take线程进来时发现有阻塞的put线程,那么他们两个就会匹配上,然后take线程获取到put线程的数据,两个线程都不阻塞。
反之一个线程调用take方法也会阻塞线程,当一个调用put方法的线程进来后也会与之匹配。
如果一个take或者put线程进来发现有同类的take或者put线程在阻塞中,那么线程会排到后面,直到有不同类的线程进来然后匹配其中一个线程
简单言之:SynchronousQueue要求生产者线程生产消息时必须要有一个消费者线程去消费
执行流程
1、主线程将请求丢给线程池
2、初始线程池线程为空时或线程池中无空闲线程是 步骤2会失败,便会创建新线程消费当前任务
3、线程空闲时间为60s,当任务来到时,会不断尝试从池中匹配空闲线成去消费SynchronousQueue元素,如某线程超过60s未执行任务,其就会被销毁
问题剖析
1、最大线程数无限制
2、使用同步队列,故此当请求并发量上来后,会源源不断的创建线程来处理请求,如果请求并发量很大,且任务执行很长的话,巨量的线程会耗尽内存资源导致程序崩溃以及CPU调度压力激增且利用率下降
newScheduledThreadPool
定时调度线程池,可使用该线程池的线程执行定时调度任务
相关方法
指定线程数
ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
指定线程数和线程工厂
ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
内部实现
创建了一个指定核心线程数,最大线程数为Integer.MAX_VALUE
,且队列使用DelayedWorkQueue
延迟队列,但是因为DelayedWorkQueue
是一个无界队列,所以最大线程数实际也是不会生效的
DelayedWorkQueue
其是一个无界队列,内部封装了优先级队列PriorityQUeue
,会对队列中的定时任务进行排序,排序是Time小的在最前面(时间早的最先被执行)
执行流程
1、调用 scheduleAtFixedRate 固定速率 或scheduleWithFixedDelay 固定延迟后,会向线程池中的延迟队列添加一个任务
2、线程池中的线程(如线程未创建或核心线程未满在拿到任务后会新建一个线程执行)从延迟队列获取任务并执行
问题剖析
1、无界队列,如何定时任务够多可能导致内存资源紧张甚至程序崩溃
2、无法根据定时任务并发性扩容线程,任务多了的话线程压力较大,且根据定时的方法调用不同导致任务延迟
总结
初步分析完了Executors类中比较常用的四类线程池,其执行流程我们也算是有了一个初步的认知,我们再次总结一下不足
newFixedThreadPool:无界队列过多的任务导致程序崩溃,线程池无法根据并发动态扩容
newSingleThreadExecutor:无界队列过多的任务导致程序崩溃,线程池无法根据并发动态扩容
newCachedThreadPool:同步队列,过大的并发将会创建很多线程,且线程无上限,可能导致程序崩溃
newScheduledThreadPool:无界队列过多的任务导致程序崩溃,线程池无法根据并发动态扩容,且有可能导致任务延时
如果不用这个类,那么我们该怎么办呢?
我们可能需要基于业务自定义线程池,按需创建,合理省心
下边是线程池中的一些核心参数,这些参数是什么意思呢?使用线程池最核心的原因是为了异步以及充分利用多核CPU提供性能,那么其中有没有什么坑呢?请关注我的下一篇文章JAVA线程池分析实战与利弊详解