看看提交任务到线程池的源码执行流程是什么样子的
共 4457字,需浏览 9分钟
·
2021-11-08 18:35
我们在上一节中对线程池构造的核心参数进行了细致的分析,一步一图的对每个参数的含义、作用都做了图文并茂的分析,相信你已经掌握的很好了。那么本节我们就继续深入源码和原理,来一起看看任务提交到线程之后的源码执行流程具体是什么样子的。
按照惯例,我们先看一张任务执行的完整流程如图1所示。
图1
我们根据这张图中的主要节点,一步一步结合源码来分析一下。
任务提交
首先是任务提交,线程池任务提交有两种方式,execute()和submit()。首先看一下execute方法的源码。我们发现它接收的入参是一个Runnable类型。我们按照代码截图中的序号依次分析,具体分析内容如图2所示。
图2
[1]首先,一进方法就先对command进行校验,如果command为空就直接抛出NPE异常;
[2,3]然后判断一下线程池中的线程个数是否小于corePoolSize,如果满足条件就调用addWorker(command, true)方法区执行任务。这个方法实际上最终就是开启了新的线程去执行任务。
[4]如果说线程池处于RUNNING状态,也就是isRunning(c)返回true,那么就将任务添加到阻塞队列。也就是执行workQueue.offer(commmand)。
[5,6]为了确保能够准确的将任务添加成功,线程池在这里做了二次校验。这里是因为,如果将任务添加到线程池之后,有可能线程池状态已经变化了,所以要校验一下,看看当前的线程池状态还是不是RUNNING。
如果线程池状态不是RUNNING了,就把任务从任务队列中删除,也就是remove(command),然后就执行拒绝策略,也就是调用reject(command)方法。
[7]如果说线程池状态确实是RUNNING,也就是二次校验通过,那么就判断一下线程池里是否还有线程,通过WorkerCountOf(recheck) == 0来判断,如果返回true了,就说明当前线程池中是空的,没有线程。怎么办呢?其实很好理解,既然没有就添加一个就完事儿了,调用一下addWorker往线程集合里增加一个线程,如图3所示。
图3
看到了吧,实际上所谓的Workers就是一个HashSet,而Worker则本质上是一个Runnable,但是它实现了AQS,代码比较复杂,也很精巧,我们稍安勿躁继续慢慢的分析。
[8]在执行[4]的时候对任务进行添加,如果添加失败就说明任务队列已经满了,那么只要线程池中的线程数小于maximumPoolSize就继续新开线程来执行任务。
如果线程数要超过maximumPoolSize了,就需要执行拒绝策略了。
到这里我们对提交任务的代码就分析结束,这些步骤其实就是本文一开始的图1,我们看一次加深下印象。
Worker线程获取任务执行流程
通过上面的讨论以及我们在之前文章中的铺垫,大家应该都知道了任务是被线程池中的工作线程(也就是Worker线程)执行的了。那我们就来看看,Worker线程是如何获取任务然后执行的。
上面的流程中,我们已经知道了线程池是通过执行addWorker方法来增加Worker线程的,实际上Worker线程就是在这个阶段被启动的,具体我们来看看代码,如图4所示。
图4
这段代码是截取的addWorker方法,注意我们用红框圈住的三个地方,这几个地方是需要重点关注的。
首先第一个位置,我们声明了一个Worker线程,并把它持有的thread成员变量的引用,赋值给final修饰的Thread t临时变量,然后判断t是否是alive状态。如果是,那么就抛出一个IllegalThreadStateException异常,也就不用启动了,因为既然已经启动了,就无需在启动了啊。
第二个位置,将这个新的Worker线程添加到工作线程集合中,通过上文的分析我们知道它是一个HashSet。并设置WorkerAdded状态变量为true。
第三个位置,校验WorkerAdded状态变量为true成立,就通过start()方法启动工作线程,其实最终启动的是Worker内部持有的Thread成员变量。通过Worker的构造方法就能知晓其中的奥义,如图5所示。
图5
到这里,我们其实已经知道了,最终调度任务的Worker工作线程,通过构造方法我们已经知道Worker本质上实现了Runnable接口,那么就能够被Thread启动,这个想必大家都知道对吧。
图6
知道了这个,就好办了,我们回过头去研究研究,Worker的run方法。
为什么要研究run方法呢?兄弟,既然我们都说了Worker是Runnable的实现,那最终线程启动之后,调度的就是它的run方法逻辑啊,也就是说,Worker肯定是实现了run方法了。代码是不会说谎的,如图7所示。
图7
怎么样,Worker线程的确是实现了run方法了,核心逻辑都在runWorker方法里,那我们就继续深入。
图8
看到这么一大段代码,想必有的兄弟又开始发慌了。不慌,这里分享一个读源码的小技巧,“抓大放小,分析重点”。我们看源码重在找到重点,至于其他的细节,慢慢看,甚至于说,你不去深究也无伤大雅,如果钻牛角尖,非得弄清每行代码,一方面时间上不现实,一方面肯定会很辛苦,而且得不偿失啊。
从这个“抓大放小,分析重点”的原则出发,我们就关注红框圈住的两行重点代码:
第一个位置,我们把Worker的task引用赋值给了Runnable局部变量;
第二个位置要额外关注,有的兄弟不知道为什么队列中的任务会被工作线程执行,其实就是这句代码在起作用。通过调用task = getTask()方法,我截取了getTask方法的核心代码,我们能够直观的看到实际上最终是调用的workQueue.take()方法取出了队列中的任务,本质上就是生产者-消费者模型。
图9
第三个位置,执行task的run方法,也就是用户提交的真正的业务逻辑。
你可能想问了,明明这里执行的是Runnable的run方法,那工作线程到哪里去了?
兄弟,这个问题问的有水平!
我们在上面的分析中,提到了addWorker这个方法,里面就对工作线程进行了start调用,其实这里就启动了工作线程,如图10所示。
图10
我们通过一步一步走读的方式,抽丝剥茧,把Worker线程获取任务并执行的流程全面的展示了出来。通过图11来展示一下这个过程便于加深理解。
图11
通过图11表示的流程,我们主要记住一个结论:当我们需要向线程池提交任务的时候,通过调用execute()传进去的任务(Runnable或者Callable实现),最终会通过Worker的构造方法传递到Worker内部,这样当start启动的以后真正执行的就是Worker中的Runnable,也就是用户提交的Runnable。
也许你会说,听起来有些复杂啊?不好意思,代码阅读的过程本身确实不是一个简单的事情,但是我们可以把原理讲的通俗易懂。因为原理本来就是可以用很简单很直白的话讲清楚的东西。
代码实现可以很复杂,但是原理越能让人理解越说明原理是普适的。如果你对代码不能很好的理解,就把这张图记住,然后再回去看源码,相信你会把握住主脉络。毕竟俗话说,捡了芝麻,丢了西瓜。我们搞技术的可切忌这样做,抓大放小才能出奇制胜啊。
Worker线程什么时候退出
分析完Worker线程的执行,我们再趁热打铁看看Worker线程是什么时候退出的。
事实上,线程池中的线程销毁,是要依赖JVM来实现自动回收的。而线程池自身,会根据当前线程池的状态来维持一定数量的线程引用,防止该部分的线程被JVM回收掉。
如果线程池决定了哪些线程要被回收,那么就将他们的引用消除即可。
我们在之前的学习中知道,Worker线程一旦被创建好了,就会持续的轮询获取任务去执行。
对于核心线程来说,他们可以无限制的等待着任务被获取并执行,而非核心的线程则是在有限的时间内获取任务,一旦Worker无法获取到任务,也就是要获取的任务为空,循环就会结束,Worker自己就会主动的去除掉在线程池中的应用,进而被回收掉并退出。
从代码角度看一下这个过程是在什么时候发生的吧,如图12所示。
图12
我们还是回到runWorker方法,发现在try块中有个while循环,循环停止的条件就是要获取的任务为空以及通过getTask获取不到任务了,最终会进到finally块执行收尾逻辑,如图13所示。
图13
首先[1]部分的代码是说,在执行线程退出之前,线程池会先统计池子里完成任务的数量,然后通过workers.remove(w)把Worker移除掉。要注意的是在统计之前加了全局锁,保证统计的准确性。
[2]部分的代码是说,如果当前线程池状态是SHUTDOWN状态并且工作队列已经为空,或者当前线程池已经是STOP状态,或者说当前线程池中没有活动的线程,则尝试对线程池状态设置为TERMINATED。
[3]部分是说,最后还是得判断一下线程池里面的实际线程数是否小于核心的线程个数,如果是的话,就得增加线程。这里的目的是保证线程池中的核心线程数量不变。
怎么样,是不是觉得线程池没有那么可怕了?我们继续趁热打铁,通过一张图来总结一下Worker线程退出的逻辑,如图14所示。
图14
面试中如果遇到这个问题,那么就把这张图的过程说出来就可以了,你可以说,我是在阅读并理解了源码的基础上总结出来的。相信你一定能够得到面试官的青睐。
到这里,本节的文章又告一段落,感谢你一直看到现在。
确实这篇文章相对比较硬核,我们毕竟是需要通过阅读源码的方式去加深理解。通过对本节的学习,相信你对线程池提交任务的过程、Worker线程获取任务并执行的过程以及Worker线程退出过程都有了源码级的深入了解。同时笔者阅读的源码“抓大放小,分析重点”的方法,相信你也get到精髓了,希望你能够利用这种思路,征服源码,收获更多。
在之后的章节中,我们将一同从实战角度出发,研究如何自定义线程池满足不同的业务场景,敬请期待,