线程池中线程是如何实现保活和回收的

面试时经常被问的一个问题,线程池中线程是如何保活的?

这个问题对于看过线程池源码的同学应该已经知道答案了,没有看过源码的也不要慌,现在我们就一起看一下线程池线程的保活策略。

image-20241008222341239

一、线程池中在哪执行任务

首先进入ThreadPoolExecutorexecute方法。

image-20241007133410861

  • 首先检查当前工作线程数量,workerCountOf(c) 是否小于核心线程数量corePoolSize,如果小于就创建一个新线程addWorker()执行这个任务。

  • 如果线程池在运行且任务加入队列成功,但是当前工作线程数量为0,也会创建一个新线程addWorker()

  • 如果任务不能被加入任务队列(workQueue.offer(command)返回false), 也会创建一个新线程addWorker()

execute方法内部有三处调用addWorker()方法的位置,进入到addWorker() 方法中,可以看到有这样一行代码new Worker(firstTask)

image-20241007135450323

Worker类又实现了 Runnable,所以线程池中的线程也就是Worker,而执行就在run()方法内部,我们只需要找到Worker类中的run方法即可。

Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

        /** Delegates main run loop to outer runWorker  */
        public void run() {
            runWorker(this);
        }

run 方法内部调用了 runWorker,秘密就在runWorker方法内部。

所以线程池中线程最终就在runWorker方法中执行的。

二、getTask 获取任务方法

在上面ThreadPoolExecutorexecute方法中,有一步是往阻塞队列中放入任务(workQueue.offer(command))。

image-20241007174135892

上面我们找到了线程池中线程执行任务的地方,那么我们看看是从哪读取的任务?

runWorker方法中有一行代码task = getTask(),此处就是从阻塞队列中获取任务的代码,让我们看一下 getTask() 的内部实现。

image-20241007181525394

核心代码就是

Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();

根据timed 的状态判断,当工作线程数量大于核心线程数量时调用poll方法获取任务,当工作线程数量小于和核心线程数量时调用take方法。

对于阻塞队列的介绍如下:

  • poll 方法如果队列不为空,返回头部元素。如果队列为空会将线程阻塞在此处,阻塞时间是keepAliveTime。当时间到了获取不到任务时返回null

  • take方法如果队列不为空返回头部元素。如果队列为空会将线程阻塞在此处,直到队列中有元素可供使用。

对于非核心线程,当线程池中的线程数量超过核心线程数量且空闲时间超过keepAliveTime时,非核心线程会被回收。

通过这种方式,线程在没有任务时就阻塞在队列上,从而实现保活。

上面我们知道了非核心线程的保活策略,那么对于核心线程又是如何保活的呢?

三、核心线程如何实现的保活

在线程池中核心线程默认是不会被回收的。

不过我们可以通过设置allowCoreThreadTimeOut来实现,getTask方法中timed的校验。

boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

所以当allowCoreThreadTimeOut 设置为true时,核心线程在没有任务可以执行时会使用take方法进行阻塞,直到获取到任务位置,而不是因为没有任务就被销毁,从而实现的线程保活。

任务执行的过程是无法保证不出问题的,不管是核心线程还是非核心线程,当线程中出现异常之后,线程池是如何处理的呢?

四、线程异常之后如何保活

继续回到runWorker方法中,其中task.run()是真正执行任务的地方,当此处发生异常之后,有try catch

image-20241007190132653

所以当任务执行异常之后,会依次执三个finally块中的代码。

再看processWorkerExit() 方法之前,我们先看一个参数completedAbruptly

completedAbruptly 代表是否是异常退出的,默认是true代表异常退出。

runWorker方法中,如果线程任务是正常执行完成的,会在最后修改该值。

image-20241007190945452

由于我们是异常线程,所以代码肯定不会走到这,直接走到finally中的processWorkerExit方法。

processWorkerExit方法中,它会根据completedAbruptly的值来调整线程池中的工作线程数量,从工作线程集合中移除该线程,并根据线程池的状态和工作线程数量决定是否需要添加新线程。

image-20241007191311126

!completedAbruptly 判断工作线程是不是异常退出的,如果不是异常退出的计算最小线程数量。

如果允许核心线程回收allowCoreThreadTimeOut=truemin0

如果min0 且工作队列不为空! workQueue.isEmpty()min1

如果当前工作线程workerCountOf(c)大于等于这个最小的线程数量min,直接返回。

如果小于这个最小的工作线程数量min,调用addWorker

此处addWorker 的触发条件就是当线程池的状态小于STOP 也就是线程池还在运行runStateLessThan(c, STOP)时且不满足上述不需要添加新线程的判断。

当上述条件满足的时候,则调用addWorker(null,false)添加一个新的工作线程,因为传入的参数Runnablenull,所以这个新线程会从任务队列中继续读取任务来执行。

最后总结一下,当线程异常之后,按照正常情况来说线程就直接消失了,但是通过processWorkerExit方法的补救,增加了一个新的线程,保证线程池的运行。

五、总结

线程池中的线程分为核心线程与非核心线程。

核心线程默认不回收,可以通过设置allowCoreThreadTimeOuttrue 来回收。

非核心线程在获取任务为空且空闲时间超过一定时间之后进行回收。

线程池的保活策略通过阻塞队列的阻塞特性实现,poll 方法实现可以指定超时时间的阻塞,take 方法实现阻塞直到获取到任务。

当线程异常之后,通过新增线程的方式实现线程的补救,保证线程池的运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值