1. Executors 创建线程池的潜在问题
- 在很多公司的编程规范中,非常明确地禁止使用Executors创建线程池。
- 为什么呢?这里从源码讲起,介绍使用Executors工厂方法创建线程池将会面临的潜在问题。
1.1 Executors 创建固定数量的线程池
的潜在问题
- 使用newFixedThreadPool工厂方法固定数量的线程池的源码如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(
nThreads, // 核心线程数
nThreads, // 最大线程数
0L, // 线程最大空闲(Idle)时长
TimeUnit.MILLISECONDS,// 时间单位:毫秒
new LinkedBlockingQueue<Runnable>() //任务的排队队列,无界队列
);
}
newFixedThreadPool工厂方法返回一个ThreadPoolExecutor实例,该线程池实例
- corePoolSize数量为参数nThread,
- maximumPoolSize数量也为参数nThread,
- workQueue属性的值为LinkedBlockingOueue
() 无界阻塞队列。使用Executors创建的固定数量的线程池的潜在问题主要存在于其workQueue上,其值为LinkedBlockingQueue(无界阻塞队列)。如果任务提交速度持续大于任务处理速度,就会造成队列中大量的任务等待。如果队列很大,很有可能导致JVM出现OOM(OutOfMemory)异常,即内存资源耗尽。
1.2 Executors 创建单线程化线程池
的潜在问题
- 使用newSingleThreadExecutor工厂方法创建单线程化线程池的源码如下
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(
1, // 核心线程数
1, // 最大线程数
0L, //线程最大空闲(Idle)时长
TimeUnit.MILLISECONDS, // 时间单位:毫秒
new LinkedBlockingQueue<Runnable>() // 无界队列new LinkedBlockingQueue<Runnable>()
));
}
通过调用工厂方法newSingleThreadExecutor()创建一个数量为1的固定大小线程池;
使用FinalizableDelegatedExecutorService对该固定大小线程池进行包装,这一层包装的作用是防止线程池的corePoolSize被动态地修改。
public void testNewFixedThreadPool2() {
//创建一个固定大小线程池
ExecutorService fixedExecutorService = Executors.newFixedThreadPool(1);
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) fixedExecutorService;
//设置核心线程数
threadPoolExecutor.setCorePoolSize(8);
//创建一个单线程化的线程池
ExecutorService singleExecutorService = Executors.newSingleThreadExecutor();
//转换成普通线程池, 会抛出运行时异常 java.lang.ClassCastException
((ThreadPoolExecutor) singleExecutorService).setCorePoolSize(8);
}
上述代码在运行时会抛出异常。可以知道FinalizableDelegatedExecutorService实例无法被转型为ThreadPoolExecutor类型,所以也就无法修改其corePoolSize属性,从而确保单线程化线程池在运行过程中corePoolSize不会被调整,其线程数始终唯一,做到了真正的Single。
反过来说,如果没有被FinalizableDelegatedExecutorService包装原始的ThreadPoolExecutor实例是可以动态调整corePoolSize属性的。
使用Executors创建的单线程化线程池与固定大小线程池一样,其潜在问题仍然存在与其workOueue属性上,该属性的值为LinkedBlockingOueue(无界阻塞队列)。如果任务提交速度持续大于任务处理速度,就会造成队列大量阻塞。如果队列很大,很有可能导致JVM的OOM异常,甚至造成内存资源耗尽。
1.3 Executors 创建可缓存线程池
的潜在问题
- 使用newCachedThreadPool工厂方法可缓存线程池的源码如下:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(
0, // 核心线程数
Integer.MAX_VALUE, // 最大线程数
60L, // 线程最大空闲(Idle)时长
TimeUnit.SECONDS, // 时间单位:毫秒
new SynchronousQueue<Runnable>()); // 任务的排队队列,无界队列
}
通过调用ThreadPoolExecutor标准构造器创建一个线程池。
核心线程数为0
最大线程数不设限制
理论上可缓存线程池可以拥有无数个工作线程,即线程数量几乎无限制。
可缓存线程池的workOueue为SynchronousQueue同步队列,这个队列入队与出队必须同时传递,正因为可缓存线程池可以无限制创建线程,不会有任务等待,所以才使用SynchronousQueue 。
当可缓存线程池有新任务到来时,新任务会被插入到SynchronousQueue实例,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可用线程则执行,若没有可用线程,则线程池会创建一个线程来执行该任务。
SynchronousQueue是一个比较特殊的阻塞队列实现类,SynchronousQueue没有容量,每一个插入操作都要等待对应的删除操作,反之每个删除操作都要等待对应的插入操作。
如果使用SynchronousQueue,提交的任务不会被真实地保存,而是将新任务交给空闲线程执行,如果没有空闲线程,就创建线程,如果线程数都已经大于最大线程数,就执行拒绝策略。使用这种队列需要将maximumPoolSize设置得非常大,从而使得新任务不会被拒绝。
使用Executors创建的可缓存线程池的潜在问题存在于其最大线程数量不设上限。
由于其maximumPoolSize的值为Integer.MAX_VALUE(非常大),可以认为是无限创建线程的,如果任务提交较多,就会造成大量的线程被启动,很有可能造成OOM异常,甚至导致CPU线程资源耗尽。
1.4 Executors 创建可调度线程池
的潜在问题
- 使用newScheduledThreadPool工厂方法可调度线程池的源码如下
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
- Executors的newScheduledThreadPool工厂方法调用了ScheduledThreadPoolExecutor实现类的构造器,而ScheduledThreadPoolExecutor继承了ThreadPoolExecutor的普通线程池类,在其构造内部进一步调用了该父类的构造器
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(
corePoolSize, // 核心线程数
Integer.MAX_VALUE, // 最大线程数
0, // 线程最大空闲(Idle)时长
NANOSECONDS, // 时间单位
new DelayedWorkQueue()); // 任务的排队队列
}
- 创建一个ThreadPoolExecutor实例
- corePoolSize为传递来的参数
- maximumPoolSize为Integer.MAX_VALUE,表示线程数不设上限
- workQueue为一个DelayedWorkOueue实例,这是一个按到期时间升序排序的阻塞队列。
- 使用Executors创建的可调度线程池的潜在问题存在于
- 其最大线程数量不设上限。由于其线程数量不设限制,如果到期任务太多,就会导致CPU的线程资源耗尽。
- 可调度线程池的潜在问题首先还是无界工作队列(任务排队的队列)长度都为Integer.MAX_VALUE,可能会堆积大量的任务,从而导致OOM甚至耗尽内存资源的问题。
2. 总结
- FixedThreadPool和SingleThreadPool
- 这两个工厂方法所创建的线程池,工作队列(任务排队的队列)长度都为Integer.MAX_VALUE,可能会堆积大量的任务,从而导致OOM(即耗尽内存资源)。
- CachedThreadPool和ScheduledThreadPool
- 这两个工厂方法所创建的线程池允许创建的线程数量为Integer.MAX_VALUE,可能会导致创建大量的线程,从而导致OOM问题。
- 通过源码分析发现,最大线程数参数maximumPoolSize对可调度线程池并未起作用
- ScheduledThreadPool内部的线程数最多为核心线程数,关键的问题还是在于其工作队列上。该线程池的工作队列(任务排队的队列)长度都为Integer.MAX_VALUE,可能会堆积大量的任务从而导致OOM问题。
- Executors工厂类提供了构造线程池的便捷方法,但是对于服务器程序而言,大家应该杜绝使用这些便捷方法,而是直接使用线程池ThreadPoolExecutor的构造器,从而有效避免由于使用无界队列可能导致的内存资源耗尽,或者由于对线程个数不做限制而导致的CPU资源耗尽等问题。所以,要求使用标准构造器ThreadPoolExecutor创建线程池。