Java线程池原理(二)
承接上文,我们知道了线程池可以复用线程去执行任务,Executors
工具类通过设置不同的参数来得到一些特性的线程池,那么继续深入,了解线程池是如何实现线程复用、核心线程的维持、非核心线程的存活控制,这些核心功能整体是如何运作的。
线程复用、核心线程的维持、非核心线程的存活控制
线程复用
此小节重点讲线程池线程复用的机制,遇到一些影不易读懂的代码我们可以跳过。
首先,线程池executor方法是线程池的入口,顺着该方法分析,不难发现一个关键的方法:addWorker(Runnable firstTask, boolean core)
,这个方法是负责添加Worker以执行任务的。
那么我们目的很明确,在此只需要关心3个点:1、具体添加了什么。2、任务怎么被执行的。3、firstTask和core参数怎么运用的。
ThreadPoolExecutor的addWorker方法
这里我们先入为主一波:
猜测该方法添加了一个叫Worker的东西,Worker内部使用一个boolean变量标识其内部Thread的性质是核心还是非核心线程,按照我们的常规的编程思路确实容易这样想。
接下来分析addWorker方法。
1 | private boolean addWorker(Runnable firstTask, boolean core) { |
总结:addWorker方法添加一个Worker
到ThreadPoolExecutor
保存worker的一个HashSet中,之后Worker
内的Thread被启动。而firstTask参数作为Worker
构造参数被使用,core参数仅参与了一个cas自旋操作,和Worker
没啥关系。
显然,我们先入为主的想法并不正确。
前文讲到线程池内是通过Worker
干活的,其内部有一个Thread在工作,Worker
是线程池实现线程复用的关键。要了解线程池线程复用等原理势必要先了解Worker
对象如何工作的。
Worker
Worker
类是ThreadPoolExecutor
的私有非静态内部类,继承了AQS且实现了Runnable
接口,Java中的AQS在此不展开讲,到这里我们只需知道Worker
继承了AQS使自身可对外提供加解锁的方法,外部的合理编码保证并发访问的安全性即可,即使不太了解AQS也没关系。通过Worker
类的方法名也可大致知道是干什么的。
1 | private final class Worker extends AbstractQueuedSynchronizer implements Runnable { |
很显然,Worker
内部的Thread运行时,会调用Worker
自身的run方法,看看Worker
的run方法做了哪些事。
1 | public void run() { |
ThreadPoolExecutor的runWorker方法
来到ThreadPoolExecutor
的runWorker方法,这个方法极其关键!!!
1 | final void runWorker(Worker w) { |
到这里,线程复用的机制很明了了:通过循环不断从工作队列中获取任务并执行;
不难总结出Worker
的工作机制:worker通过实现Runnable接口,内部维护一个Thread线程对象,将自身作为“跳板”,优先执行自身初始化时携带的任务,之后不断消费(执行)线程池工作队列中的任务,直到队列中任务没有任务为止,worker的任务就结束了。
用图表示ThreadPoolExecutor
的runWorker方法主要的执行过程如下:
不过目前为止,我们还是没发现和核心线程与非核心线程相关的痕迹,带着疑问继续往下走。
核心线程的维持、非核心线程的存活控制
阅读ThreadPoolExecutor
的runWorker方法,不难发现,每个Worker
对象结束使命后,都会走到ThreadPoolExecutor
的processWorkerExit方法执行退出逻辑。该方法入参除了Worker
对象以外还需要传入一个boolean类型的completedAbruptly参数,见明之意,该参数表示该Worker
对象是否是“突然结束”,不难发现只有任务task执行期间抛出异常或error,Worker
对象才算是“突然结束”,completedAbruptly为true,反之正常结束,completedAbruptly为false。
ThreadPoolExecutor的processWorkerExit方法
综上所述,processWorkerExit方法职责就是处理Worker
对象正常与非正常的结束,也可以理解为对Worker
的回收。
1 | private void processWorkerExit(Worker w, boolean completedAbruptly) { |
小总结一下processWorkerExit主要干了几件事:
- 记录
Worker
对象完成的任务数到总记录中 - 移除该
Worker
对象 - 处理非正常结束
Worker
对象时,调整线程池记录的worker数量,补充一个新的worker且是以非核心线程的设定去补充的
提一下为什么Worker
非正常结束时要补充新的Worker
:
- 再仔细看runWorker方法,因为
Worker
非正常结束说明任务task的run方法抛出了异常(或error),尽管做了try-catch处理,作者的做法是把异常继续往抛出,而处理Worker
的回收工作processWorkerExit方法是在finally块执行的,也就是说processWorkerExit方法执行完之后,异常就要被抛出去了,虚拟机接受到异常之后,就会销毁该线程,也就是销毁Worker
内的Thread(线程死亡),此时需要补充新的Worker
继续干活。 - 此处给读者留下一个小思考:既然runWorker方法catch到了异常或error,可以不抛出去吗?
不过跟踪到这里还是没发现核心线程与非核心线程相关的操作,而且在补充新的worker时,还是以非核心线程去补充的,这也是疑问点,并且如果每个Worker
使命结束后都要移除了,Worker
内的线程也执行完代码也要结束生命了,说好的核心线程不会被销毁呢?
ThreadPoolExecutor的getTask方法
回过头看,我们还差一个方法没有分析,那就是ThreadPoolExecutor
的getTask方法,在runWorker方法中Worker
通过getTask从工作队列中获取任务,不难得出:如果获取的任务为null,Worker将被回收。而工作队列是阻塞队列,所以如果Worker
在获取任务时被阻塞,那么就不会被回收,从而实现Worker
内Thread的“常驻”效果,这确实是个思路。话不多说进源码。
下列代码笔者将保留原生的官方英文注释,增加自己的注释。
1 | private Runnable getTask() { |
这段代码比较短但是比较精妙,需要反复揣摩,下面贴出笔者制作的流程图
到此,终于看到线程池七八成左右的真身了:runWorker方法使worker不断执行任务和任务通知,processWorkerExit方法负责worker的回收工作,而getTask方法负责从工作队列获取任务进一步控制worker内Thread的阻塞等待,进一步控制worker的回收,从而实现核心线程的常驻效果和非核心线程的存活控制。
提交优先级与执行优先级
有如下程序
1 | public static void main(String[] args) { |
问:提交到第几个任务(i = ?)时,线程池拒绝执行任务而抛出异常?
答:i = 31时。
线程池可接受的最大任务数量为:最大线程数+工作队列容量=30。上述每个任务都进行了一次长时间睡眠,显然for循环体积任务执行完之前,线程池内没有一个任务执行完。
值得注意的是,控制台打印的“start run number: {n}”字符串,应该是1到10左右先被打印,其次是21到130后被打印,最后是11到120区间的数字被打印,而不是我们生活中任务认为的“先提交就先得到执行”。
于是结合addWorker方法得出线程池提交优先级:
核心线程 > 工作队列 > 非核心线程
于是结合getTask方法得出线程池执行优先级:
核心线程 > 非核心线程 > 工作队列
小结
通过以上4个方法的解读,我们可以重新认识worker对象:
- 通俗的说每个worker被创建之后,其使命就是不断从工作队列领取任务去执行,在无任务可执行(不是任务全部执行完毕,两者概念差别很大),即领取不到任务之后,就要被回收。
不仅如此,核心线程与非核心线程也要重新理解:
- 核心线程:worker对象内的线程在获取任务前,线程池状态处于running,此时若当前worker数(反应线程池内的线程数)小于等于设置的核心线程数,该线程以poll方式方式获取任务,若工作队列无任务,则该线程将一直等待新的任务提交进来,该线程也就顺理成章的成为线程池常驻线程,也就是核心线程
- 非核心线程:worker对象内的线程在获取任务前,线程池状态处于running,此时若当前worker数(反应线程池内的线程数)大于设置的核心线程数,该线程以take方式方式获取任务,若工作队列无任务,则该线程将等待新的任务提交进来,等待时长为keepAliveTime,超时之后便会被对应worker被回收,等待期间该线程也就成为线程池非核心线程。
所以每个worker内的线在启动工作期间都是相同的性质,不分核心与非核心,只有工作队列无任务可执行时,才能明确线程的性质是核心还是非核心,同时也决定了worker的回收与否(既分高下,也决生死?!)。
回过头来看addWorker方法中表示线程类型的core参数,也只是参与了当前worker数与核心线程数或非核心线程数的比较,再无他用,这也说的通了。
在阅读这些方法中,我们跳过了一些“影响阅读”的代码,如worker自身的lock和unlock究是为了防止啥,ThreadPoolExecutor
成员变量中ReentrantLock
的mainLock保证了什么的线程安全,以及线程池自身的其它功能,如线程池的worker数量和状态等如何感知的……请听下回合分解。