Executors和ThreaPoolExecutor创建线程池的区别

timo-nbktp 1年前 ⋅ 1155 阅读

在很多公司的编程规范中,非常明确地禁止使用Executors快捷创建线程池,为什么呢?这里从源码讲起,介绍使用Executors工厂方法快捷创建线程池将会面临的潜在问题。

1. 使用Executors创建“固定数量的线程池”的潜在问题

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

newFixedThreadPool工厂方法返回一个ThreadPoolExecutor实例,该线程池实例的corePoolSize数量为参数nThread,其maximumPoolSize数量也为参数nThread,其workQueue属性的值为LinkedBlockingQueue<Runnable>()无界阻塞队列。

使用Executors创建“固定数量的线程池”的潜在问题主要存在于其workQueue上,其值为LinkedBlockingQueue(无界阻塞队列)。如果任务提交速度持续大于任务处理速度,就会造成队列中大量的任务等待。如果队列很大,很有可能导致JVM出现OOM(Out Of Memory)异常,即内存资源耗尽。

2. 使用Executors创建“单线程化线程池”的潜在问题

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}

以上代码首先通过调用工厂方法newFixedThreadPool(1)创建一个数量为1的“固定大小的线程池”,然后使用FinalizableDelegatedExecutorService对该“固定大小的线程池”进行包装,这一层包装的作用是防止线程池的corePoolSize被动态地修改。
使用Executors创建的“单线程化线程池”与“固定大小的线程池”一样,其潜在问题仍然存在于其workQueue属性上,该属性的值为LinkedBlockingQueue(无界阻塞队列)。如果任务提交速度持续大于任务处理速度,就会造成队列大量阻塞。如果队列很大,很有可能导致JVM的OOM异常,甚至造成内存资源耗尽。

3. 使用Executors创建“可缓存线程池”的潜在问题

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

以上代码通过调用ThreadPoolExecutor标准构造器创建一个核心线程数为0、最大线程数不设限制的线程池。所以,理论上“可缓存线程池”可以拥有无数个工作线程,即线程数量几乎无限制。“可缓存线程池”的workQueue为SynchronousQueue同步队列,这个队列类似于一个接力棒,入队出队必须同时传递,正因为“可缓存线程池”可以无限制地创建线程,不会有任务等待,所以才使用SynchronousQueue。

当“可缓存线程池”有新任务到来时,新任务会被插入SynchronousQueue实例中,由于SynchronousQueue是同步队列,因此会在池中寻找可用线程来执行,若有可用线程则执行,若没有可用线程,则线程池会创建一个线程来执行该任务。

SynchronousQueue是一个比较特殊的阻塞队列实现类,SynchronousQueue没有容量,每一个插入操作都要等待对应的删除操作,反之每个删除操作都要等待对应的插入操作。也就是说,如果使用SynchronousQueue,提交的任务不会被真实地保存,而是将新任务交给空闲线程执行,如果没有空闲线程,就创建线程,如果线程数都已经大于最大线程数,就执行拒绝策略。使用这种队列需要将maximumPoolSize设置得非常大,从而使得新任务不会被拒绝。

使用Executors创建的“可缓存线程池”的潜在问题存在于其最大线程数量不设限上。由于其maximumPoolSize的值为Integer.MAX_VALUE(非常大),可以认为可以无限创建线程,如果任务提交较多,就会造成大量的线程被启动,很有可能造成OOM异常,甚至导致CPU线程资源耗尽。

4. 使用Executors创建“可调度线程池”的潜在问题

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, NANOSECONDS,
          new DelayedWorkQueue());
}

以上代码创建了一个ThreadPoolExecutor实例,其corePoolSize为传递来的参数,maximumPoolSize为Integer.MAX_VALUE,表示线程数不设上限,其workQueue为一个DelayedWorkQueue实例,这是一个按到期时间升序排序的阻塞队列。

使用Executors创建的“可缓存线程池”的潜在问题存在于其最大线程数量不设限上。由于其线程数量不设限,如果到期任务太多,就会导致CPU的线程资源耗尽。

5. 总结

以上内容分别梳理了Executors四个工厂方法所创建的线程池将面临的潜在问题。总结起来,使用Executors创建线程池主要的弊端如下:

  • FixedThreadPool和SingleThreadPool,这两个工厂方法所创建的线程池,工作队列(任务排队的队列)的长度都为Integer.MAX_VALUE,可能会堆积大量的任务,从而导致OOM(即耗尽内存资源)。
  • CachedThreadPool和ScheduledThreadPool,这两个工厂方法所创建的线程池允许创建的线程数量为Integer.MAX_VALUE,可能会导致创建大量的线程,从而导致OOM。

虽然Executors工厂类提供了构造线程池的便捷方法,但是对于服务器程序而言,大家应该杜绝使用这些便捷方法,而是直接使用线程池ThreadPoolExecutor的构造器,从而有效避免由于使用无界队列可能导致的内存资源耗尽,或者由于对线程个数不做限制而导致的CPU资源耗尽等问题。

所以,大厂的编程规范都不允许使用Executors创建线程池,而是要求使用标准构造器ThreadPoolExecutor创建线程池。

--end--

 

版权 本着开源共享、共同学习的精神,本文转载自 https://hengheng.blog.csdn.net/article/details/123373042 , 如果侵权之处,请联系博主进行删除,谢谢~