线程池的生命周期

一、线程池生命周期概述

线程池的运行状态,并不是用户显示设置的,而是伴随着线程池的运行,由内部来维护。

源码中在对ctl变量做解释的时候讲到了下面的五种状态做了解释,下面的描述无非就是将文字转换成流程图而已。

线程池内部使用一个变量维护两个值,将运行状态(runState)和线程数量(workerCount)放在同一个32位的int类型的变量中,前三位代表状态,后29位代表线程的数量:

//ctl变量用来同时表示运行状态和线程数量,高三位表示线程的状态runState,低29位表示线程的数量
AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

// COUNT_BITS = 29
 COUNT_BITS = Integer.SIZE - 3;

 // 左边三个0,右边29个1,用来表示线程的最大数量
 CAPACITY   = (1 << COUNT_BITS) - 1;

 //左边111右边都是0
 RUNNING    = -1 << COUNT_BITS;

 //都是0
 SHUTDOWN   =  0 << COUNT_BITS;

 //左边001右边都是0 
 STOP       =  1 << COUNT_BITS;

 //左边010右边都是0
 TIDYING    =  2 << COUNT_BITS;

 //左边011右边都是0
 TERMINATED =  3 << COUNT_BITS;

// Packing and unpacking ctl
// 获取运行时的状态
private static int runStateOf(int c)     { return c & ~CAPACITY; }

//获取线程数量
private static int workerCountOf(int c)  { return c & CAPACITY; }

//通过运行时状态和线程数生成ctl
private static int ctlOf(int rs, int wc) { return rs | wc; }

对计算出来的workercount的说明:workerCount是允许启动和不允许停止的工作数。该值可能暂时不同于实际的活动线程数,例如,当线程工厂在被请求时无法创建线程时,以及当退出线程在终止前仍在执行簿记时

为什么用同一个变量保存两个值?

可以避免做相关决策时,出现不一致的情况,不必维护两者的一致,而占用锁资源;

源码中经常出现判断线程池的运行状态和线程数量的情况;

所以为了提高效率,采用了一个变量来保存两个值。

为什么使用原子类(AtomicInteger)和大量使用位运算?

1、避免使用加锁来操作;

2、因为加锁需要耗时,而原子类操作本身就自带原子性;

二、ThreadPoolExecutor的五种状态

源码中在对ctl变量做解释的时候讲到了下面的五种状态做了解释,下面的描述无非就是将文字转换成流程图而已。

生命周期状态切换如下图所示:

对五种状态来做说明:

状态 运行状态描述
RUNNING 接受新任务和处理排队任务
SHUTDOWN 不接受新任务,但处理排队任务
STOP 不接受任务,不处理排队任务,中断正在进行的任务
TIDYING 所有任务都已终止,workerCount为零
TERMINATED 转换到TIDYING状态的线程将运行terminated()钩子方法

三、RUNNING->SHUTDOWN和STOP

因为两个方法太相似了,所以决定放在一起讲。把这两个状态分析完成之后,后面的两个状态顺带着也就给讲了。

线程池调用shutdown方法和stop方法

1、修改ctl的值

看一下这里的代码非常的熟悉

那么只需要看下advanceRunState方法到底在做什么事情即可。

对于targetState变量来说,当前有两种值的选择:SHUTDOWN或者是STOP(注释中已经注明,不可能是TIDYING或者是TERMINATED)

runStateAtLeast:根据参数比较一下线程池中的状态;其实就是数字的比较而已,这个可能在执行阶段会有波动。

workerCountOf:计算当前线程池中的有效的线程数量(该值可能暂时不同于实际的活动线程数)

参数 方法描述
SHUTDOWN ctlOf(targetState, workerCountOf(c)):返回的是线程池中有效的线程数量
STOP ctlOf(targetState, workerCountOf(c)) = 2^29 + 2 , 为STOP状态

打上断点测试一下

targetState为SHUTDOWN时

targetState为STOP时

2、shutdown方法中的interruptIdleWorkers方法

然后对于shutdown方法来说,会执行interruptIdleWorkers()方法,中断空闲线程;只需要看下具体的实现即可

将此时此刻的线程池中的线程划分成三种状态:新任务正在被新建的线程准备处理线程正在运行线程然后可以从队列中获取得到空闲线程

新任务正在被新建的线程准备处理

这里又分为了几个细节。但是大前提是不变的,先来看下addWorker方法

因为当前线程池状态就是SHUTDOWN且工作线程的第一个任务不为空,而返回false。

此时有两种处理方式:直接被拒绝或者是任务丢弃,在execute方法中体现出来的。

但是如果刚刚好线程池状态更新为SHUTDOWN之后,因为在创建worker的时候不允许被中断,会让这个线程将当前这个任务执行结束。

在addWorker下面没有找到任何方法对这种情况做了判断了的。

线程正在运行线程然后可以从队列中获取得到

如果在任务执行期间,SHUTDOWN中断的是空闲线程,那么当线程执行完成任务之后,会再尝试从队列中获取得到任务。

那么需要关注的是runWorker方法中的while循环。因为while循环中没有关于SHUTDOWN状态的说明,那么看下getTask中的说明

线程继续处理任务,当把工作队列中的任务处理完成之后,有效线程减少一个。返回空任务。

然后会进入到processWorkerExit方法中,然后看下具体的执行逻辑:

因为不允许核心线程消亡,所以这里才去了一种比较柔性的方式。

执行下面的addWorker方法,不给任务,然后来判断

至此核心线程消亡,核心线程也会慢慢减少至0。

空闲线程

所谓的空闲线程并不是什么都不做,而是在任务执行结束,正准备从队列中获取得到任务这个中间过程中。

因为只有这个阶段中,才是无锁的。这里的线程中断其实就是一个线程中断而已。

在整个代码中,在执行的过程中反而把线程标记给擦除掉了。

但是擦除了的线程并非是说,将线程中断暴露给外界,依然是自己使用了。

被中断了线程已经被擦除了,所有无法在业务代码中感知到。

那么这么做的目的是什么呢?就是为了不接受新的任务,然后让其他的线程将所有的已经有的任务给处理掉。

就做了这么一个需求而已。但是我的问题是为什么不在添加新任务的时候直接给拒绝。

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

这里也有两种情况:一种是新来的;一种是队列中已经没有了

我感觉这两种判断已经足够了,但是为什么在这里还要中断线程呢?

所以我做了一番挣扎思考,我将问题来描述总结一下:

线程池调用了shutdown方法之后,在中断空闲线程时队列还有任务,被打上中断标记的线程在第二轮循环中可以从任务队列中获取得到任务执行,然后执行任务的时候又把线程中断标记给修改成正常的了。那既然是这样子操作,为什么还要中断空闲的线程呢?

我的想法是所有的线程把当前线程的任务执行完成之后,线程都在等待从队列中获取得到任务,但是最后一个线程执行完任务后发现,当前队列中没有任务了,就去唤醒阻塞在队列上的线程,不就可以了么。为什么要多一个中断线程的步骤呢?

所谓的空闲线程就是队列为空时,阻塞在阻塞队列上的线程。

代码的意思应该是想让队列中的任务早点完结,不管是在没有过去得到队列中的任务之前还是已经阻塞在队列上,都没有问题的。

总结

正在RUNNING的线程,线程池调用了shutdown方法之后,当队列任务为空的时候,都会阻塞在队列上。

而每个线程消亡的时候,都会进入到processWorkerExit方法中来

然后调用tryTerminate

然后其他循环在跳出for循环的第二轮之后,会再次判断:因为当前是SHUTDOWN状态并且队列为空,那么就从跳出while循环

所以线程池从RUNNING转换成shutdown状态的时候,最终的状态是:任务队列中没有了任务,线程都在等待着从队列中获取得到任务。

3、shutdownNow方法中的interruptWorkers方法

对于shutdownNow方法来说,首先会改变线程池状态,然后会执行interruptWorkers()方法,中断所有的线程;

中断线程。这里又分成了三种情况:新任务正在被新创建的线程持有准备执行、线程正在运行任务、空闲线程

新任务正在被新创建的线程持有准备执行

和shutdown的一致,但是状态是STOP而已

线程正在运行任务

这里又分为两种情况:

1、刚刚从队列中获取得到任务准备执行;

2、正在执行任务期间。运行任务期间,也就是run方法运行期间,这时候,可以选择在代码中来进行判断。

开发者可以有选择的来选择进行丢弃,还是继续进行操作。

无论哪种情况,线程池选择的做法是虽然中断了线程,然后依然选择让中断了的线程将任务执行结束。

而在执行过程中,将中断标记给了线程,让线程能够感知到中断了,让开发者自由选择是否选择中断。

空闲线程

和shutdown方法一致;

所以RUNNING到STOP状态的时候,不管当前队列中是否有任务,中断所有线程。空闲线程直接返回为空

而正在执行线程的任务可以执行任务,但是依然在运行任务,具体的选择取决于开发者来选择执行任务是否做了判断,如果没有判断,那么可以将任务执行完成。

但是执行完了任务,就无法再去队列中获取得到任务。也是中断。

然后尝试终止。

进入到TIDYING状态。

shutdown和shutdownNow的区别

1、shutdown表示中断空闲线程;shutdownNow中断的是所有线程;

2、shutdown表示让所有线程先把队列中的任务执行完成之后,再中断空闲线程;shutdownNow表示直接中断空闲线程之后,不再去跑任务,正在执行任务的线程让开发者自己来管理,执行完成之后,再去执行任务的时候,发现已经为STOP状态,直接返回;

3、shutdown选择是将任务执行完成;而shutdownnow选择了将队列中的任务保存起来;回头来恢复执行即可;

这就是区别shutdown和shutdownnow的区别,至此全部结束。

四、代码演示

调用shutdown方法

从最终执行图上来看,即使中断了空闲线程,但是依然会有线程来帮忙执行队列中的任务,直到队列中任务为空;

调用shutdownNow方法

从这里可以看到执行到最终之后:

1、任务队列中还有一个任务没有被处理。而把任务给暴露出来了,让线程线程决定如何处理;

2、正在执行任务的线程已经终止了,可以有选择性的来执行里面的代码;

五、流程图总结

posted @ 2022-10-27 21:20  写的代码很烂  阅读(163)  评论(0编辑  收藏  举报