【Java 线程池】【十】线程池篇总结以及为什么不提倡使用Executors来构建线程池

发布时间 2023-04-14 07:14:30作者: 酷酷-

1  前言

这节也是我们线程池的最后一节咯,我们这节来总结一下。

2  线程池总结

线程池篇我们讲解了两种线程池,一种是ThreadPoolExecutor线程池、另外一种是ScheduledThreadPoolExecutor线程池。

2.1  ThreadPoolExecutor 线程池

关于ThreadPoolExecutor我们讲解了它的基本使用方式、内部的核心参数、提交任务的execute、submit方法源码流程。
其中submit方法内部也是会调用execute方法去提交任务的,如下图所示:

对于ThreadPoolExecutor线程池提交任务如下:
(1)首先判断 当前线程 < corePoolSize的时候,就会创建出来一个工作者Worker,每个Worker内部有一个线程池,然后将提交的任务交给这个worker,作为这个worker的第一个任务去执行
(2)当线程数量 >= corePoolSize的时候,提交任务会先尝试存放入阻塞队列,调用阻塞队列的offer方法提交任务,如果阻塞队列未满此时提交任务成功,返回
(3)当阻塞队列满了,此时会尝试再去创建新线程出来,如果线程数量 < maximumPoolSize的时候,可以创建成功,将任务交给新的工作者执行。如果 线程数量 >= maximumPoolSize,此时已不允许创建新线程,表示线程池已经饱和了,只能执行拒绝策略。

我们还看了调用submit提交任务的时候,任务会被封装成一个FutureTask任务。同时分析了FutureTask内部的原理:

(1)FutureTask内部有一个state变量表示任务状态,NEW未执行完成(可能刚创建、也可能是执行中)、COMPLETING表示执行完成正在完成、NORMAL表示已完成。
(2)FutureTask内部有一个outcome属性保存任务的执行结果,当任务已完成,调用FutureTask的get方法返回outcome结果
(3)当任务未完成,为NEW或者COMPLETING的时候调用get方法会被阻塞住,内部使用LockSupport.park方法阻塞
(4)当线程池执行完任务的时候,会调用LockSupport.unpark方法唤醒那些被阻塞住的线程

还看了ThreadPoolExecutor内部的Worker工作者的原理,Worker内部有一个工作线程,工作线程启动的时候会执行runWorker方法,会不断的从阻塞队列中取出任务,然后执行任务,如下图:

 还看了ThreadPoolExecutor线程池的两种销毁方式:
(1)shutdown方法优雅关闭线程池,不会中断正在任务的线程,等提交到线程池的所有任务都执行完毕才会关闭
(2)shutdownNow方法暴力关闭线程池,存储与阻塞队列还未执行的任务全部被清空,同时被暴力调用线程池中每个线程的interrupt方法中断所有线程,包括正在执行任务的线程

2.2  ScheduledThreadPoolExecutor 线程池

关于ScheduledThreadPoolExecutor线程池,我们讲解了它提交任务的方法execute、submit、schedule、scheduleAtFixRate方法;
其中execute、submit都是调用schedule方法去提交一个延迟任务的,只不过这个延迟任务的延迟延迟时间为0而已。

大致的执行过程如上图:
(1)schedule、scheduleAtFixRate方法都会将任务封装成一个ScheduleFutureTask任务,然后调用delayedExecute方法提交到线程池
(2)提交任务的时候,首先将任务存储到延迟阻塞队列DelayedWorkQueue中
(3)提交到队列后,判断当前线程数为0 或者 当前线程数 < corePoolSize 就创建一个新线程出来
(4)新线程创建出来之后就存在线程池中,会调用上述讲解过的runWorker方法不断从阻塞队列中取出任务来执行

看了ScheduleFutureTask内部的原理:
(1)实现了Delayed接口,具有延迟时间的属性
(2)实现了RunnableScheduleFuture接口,具有调度属性,能判断任务是周期性任务还是一次性任务
(3)内部优先使用延迟时间作为排序优先级,延迟时间小的任务优先级高;如果延迟一样,则按照sequenceNumber 排序,越小优先级越高。

看了DelayWorkQueue内部的原理:
(1)实现了BlockingQueue接口,是一个阻塞队列
(2)内部使用了一个数组存储任务,同时使用堆排序根据延迟时间、sequenceNumber进行排序,构建小顶堆,延迟时间最小的任务在堆顶
(3)对于offer、poll、take方法的实现跟DelayQueue延迟队列实现基本一致,对于DayedQueue的实现在并发数据那里有详细的分析

3  为什么不建议使用Executors来构建线程池

接下来我们讲解一下JDK提供的Executors这个工具类,提供创建线程池的几个方法,之前我们讲解的时候不建议使用Executors来构建线程池,我们来分析一下为什么:

3.1  newCachedThreadPool 方法

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

这里注意 corePoolSize为0,maximumPoolSize为MAX_VALUE、阻塞队列是SynchronousQueue
(1)由于当前线程数始终大于等于0,也就是当前线程数 >= corePoolSize永远成立
(2)所以提交任务的时候,优先提交到阻塞队列中
(3)队列使用的是SynchronousQueue同步队列,这种队列的特点之前在并发数据容器篇的时候已经深入分析过了;
只有当别的线程正在调用poll方法或者take方法的时候,当前线程调用offer方法才会成功。
(4)所以最开始是任务放入阻塞队列是失败的,就会触发另外一个条件,线程数 < MAX_VALUE能创建线程成功。
(5)这就导致,如果一下子提交非常多的任务,会创建非常多的线程
(6)线程数量太多,cpu竞争剧烈,可能cpu 100%;同时由于每个线程数量需要占用一定内存,默认1M,所以可能导致内存资源耗尽而OOM

3.2  newFixdThreadPool 方法

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

最小、最大线程数为nThreads,也就是线程数是固定的,非常注意的是这里使用的是LinkedBlocingQueue无界阻塞队列
(1)线程数量 < corePoolSize,提交任务会创建新线程
(2)当线程数量达到 corePoolSize时候,提交任务会尝试存入阻塞队列中
(3)非常要注意这里LinkedBlockingQueue使用的是默认构造函数,阻塞队列最大容量是Integer.MAX_VALUE,相当于容量没有限制
(4)如此会造成大量任务进入LinkedBlockingQueue队列,而其又在内存中,会造成OOM
(5)很常见的情况,我们线程就有过几次由于开发人员对LinkedBlockingQueue容量设置没有上限、或者设置过大而造成的OOM事故,这里要非常注意

3.3  newSingleThreadExecutor 方法

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

这里使用的一样是LinkedBlockingQueue无界队列(容量大小没有限制,默认是Integer.MAX_VALUE),一样会导致上面的问题。

3.4  newScheduledThreadPool 方法

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

public ScheduledThreadPoolExecutor(int corePoolSize) {
    // 内部调用父类ThreadPoolExecutor线程的构造方法
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}

这里使用的是DelayedWorkQueue延迟队列,这个队列的offer方法提交任务的时候也是没有大小限制的,如果数组大小不够就会扩容,这种情况也可能大量的任务积压在DelayedWorkQueue内存队列,从而造成OOM。
还有这里的ScheduledThreadPoolExecutor使用要比较谨慎一点,插入删除任务的复杂度我们上节对比过了,不适合提交大量的任务,会造成大量任务积压而OOM,比较适合一些任务比较少,一些少量的定时任务、延迟任务等情况。

说白了其实就是因为这些Executors创建的线程池要么就是最大线程数量设置没有上限、要么就是阻塞队列的容量设置没有上限,都有很大的风险。

4  小结

好了,关于线程池的我们就看这么多哈,技术没有捷径就是要多看多用多写,其实什么事情都是如此,多尝试多发现多思考哈,有理解不对的地方欢迎指正哈。