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

开发
面试时经常被问的一个问题,线程池中线程是如何保活的?现在我们就一起看一下线程池线程的保活策略。

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

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

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

首先进入ThreadPoolExecutor的execute方法。

  • 首先检查当前工作线程数量,workerCountOf(c) 是否小于核心线程数量corePoolSize,如果小于就创建一个新线程addWorker()执行这个任务。
  • 如果线程池在运行且任务加入队列成功,但是当前工作线程数量为0,也会创建一个新线程addWorker()。
  • 如果任务不能被加入任务队列(workQueue.offer(command)返回false), 也会创建一个新线程addWorker()。

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

而 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 获取任务方法

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

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

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

核心代码就是

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。

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

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

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

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

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

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

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

如果允许核心线程回收allowCoreThreadTimeOut=true,min为0。

如果min为0 且工作队列不为空! workQueue.isEmpty(),min为1。

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

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

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

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

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

五、总结

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

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

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

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

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

责任编辑:赵宁宁 来源: 醉鱼Java
相关推荐

2024-06-13 09:30:33

Java线程池线程

2024-08-29 08:54:35

2023-02-02 08:56:25

线程池线程submit

2024-04-02 09:53:08

线程池线程堆栈

2020-02-26 15:12:43

线程池增长回收

2021-11-29 10:55:11

线程池Java面试

2021-06-17 06:57:10

SpringBoot线程池设置

2011-06-01 11:23:09

Android 线程

2019-09-26 10:19:27

设计电脑Java

2022-10-12 09:01:52

Linux内核线程

2012-01-16 09:00:56

线程

2024-04-08 10:09:37

TTLJava框架

2023-10-26 08:25:35

Java线程周期

2021-06-24 08:02:35

线程池Java代码

2024-08-30 08:23:06

2020-10-12 08:32:34

浏览器进程线程

2024-05-20 13:13:01

线程安全Java

2022-09-06 08:25:13

线程异步任务

2022-10-24 08:03:04

MySQL数据库

2019-01-28 08:50:09

线程安全
点赞
收藏

51CTO技术栈公众号