【Java 线程池】【十】线程池篇总结以及为什么不提倡使用Executors来构建线程池
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 小结
好了,关于线程池的我们就看这么多哈,技术没有捷径就是要多看多用多写,其实什么事情都是如此,多尝试多发现多思考哈,有理解不对的地方欢迎指正哈。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了