并发编程-线程池(二)线程池回收线程
【1】https://blog.csdn.net/u013256816/article/details/109213183
面试 鹅厂 的时候,问到了 线程池如何销毁线程,这题答的不好。
这个问题考察的是对线程池的理解,在既然了解了线程池在什么时候创建线程。
那么也要了解线程池在不同情况是如何回收线程,什么时候回收,怎么回收。
一、线程池状态和状态转换
首先要了解线程池状态和线程池状态之间的转换 : https://www.cnblogs.com/Jomini/p/13669993.html
Running : 线程池的初始化状态是RUNNING, 线程池处在RUNNING状态时,能够接收新任务,以及对已添加的任务进行处理。
SHUTDOWN : 线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务,异步中断闲置的的线程
调用线程池的 shutdown() 接口时,线程池由RUNNING -> SHUTDOWN。
STOP : 线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
用线程池的 shutdownNow() 接口时,线程池由 (RUNNING or SHUTDOWN ) -> STOP。
(注: shutdown 和 shutdownNow() 的不同)
TIDYING : 当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行钩子函数terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进
行相应的处理;可以通过重载terminated()函数来实现。
当线程池在SHUTDOWN状态下,阻塞队列为空并且线程池中执行的任务也为空时,就会由 SHUTDOWN -> TIDYING。
当线程池在STOP状态下,线程池中执行的任务为空时,就会由STOP -> TIDYING。
TERMINATED : 线程池彻底终止,就变成TERMINATED状态。 线程池处在TIDYING状态时,执行完 terminated()之后,就会由 TIDYING -> TERMINATED。
二、线程池回收线程
首先分为 不调用 shutdown 和 调用 shutdown 的场景,shutdownNow 这里不分析。
然后分别,分析 Running状态, Shutdown状态, Stop 状态, 下的回收线程。
2.1、不调用 shutdown 的场景下
2.1.1、Running 状态
[1]、Running状态下,当前工作线程数量多于核心线程,且任务队列为空
当前线程数量多于核心线程,且任务队列为空,此时满足减少线程数量的判断,并减少一个线程数量,线程池再回收移除一个线程。
[1.1]、 jdk 1.8源码的具体实现【1】
runWorker(Worker w) 方法
工作线程启动后,就进入 runWorker(Worker w) 方法,也就是 Running 状态下,在runWorker(Worker w)) 中
里面是一个while循环,循环判断任务是否为空,若不为空,执行任务;若取不到任务,或发生异常,退出循环,执行processWorkerExit(w, completedAbruptly); 在这个方法里把工作线程移除掉。
取任务的来源有两个,一个是firstTask,这个是工作线程第一次跑的时候执行的任务,最多只能执行一次,后面得从 getTask() 方法里取任务。
getTask() 是关键,在不考虑异常的场景下,返回null,就表示退出循环,结束线程。
getTask() 方法
getTask() 返回null, 有以下两个条件
第一种情况,线程池的状态已经是STOP,TIDYING, TERMINATED,或者是SHUTDOWN且工作队列为空;
第二种情况,工作线程数已经大于最大线程数或当前工作线程已超时,且,还有其他工作线程或任务队列为空。这点比较难理解,总之先记住,后面会用。
在 Running 状态下,是在满足第二种情况,才会返回 null。
此时,当前线程数量多于核心线程,则 timed 为 true; 并且,任务队列为空,workQueue.isEmpty() is ture。
根据下图的源码第二部分,以上条件满足返回 null 的条件,并通过 compareAndDecrementWorkerCount(c) 方法,通过 CAS 机制减少一个线程数量,注意线程还未被回收。
如果compareAndDecrementWorkerCount(c) 失败,则 continue 进入下一次循环,
processWorkerExit(Worker w, boolean completedAbruptly) 方法
在 runWorker(Worker w) 方法中,getTask 返回 null 后,就可以进入 processWorkerExit 回收线程。
通过 workers.remove(w) 移除线程,并用了 tryTerminate()
tryTerminate()
第一个判断条件没有一个子条件符合,跳过。第二个条件,工作线程还存在,那么随机中断一条空闲线程。
InterruptdleWorkers(boolean onlyOne)
被移除出工作队列的线程,中断其他线程。
2.1.1.2、线程数量大于最大线程数量,多的直接丢弃。
2.2、调用shutdown 的场景下
这种场景,无论是核心线程还是非核心线程,所有工作线程都会被销毁。
在调用shutdown()之后,会向所有的空闲工作线程发送中断信号
最终传入false,调用下面这个方法。
2.2.1 Running 状态
2.2.1.1、 Running 状态,任务已全部完成,线程在阻塞等待。在 Running 状态进入 shutdown 状态。
很简单,中断信号将其唤醒,从而进入下一轮循环。到达条件1处,符合条件,减少工作线程数量,并返回null,由外层结束这条线程。
这里的decrementWorkerCount()是自旋式的,一定会减1。
2.2.1.2、 Running 状态,任务还没有完全执行完
调用shutdown()之后,未执行完的任务要执行完毕,池子才能结束。所以此时有可能线程还在工作。
这里又要分两个阶段讨论
2.2.1.2-1 、阶段1 任务较多,工作线程都能获得任务
这里还不涉及到线程退出,可以跳过不看,只是分析一下收到中断信号后线程的表现。
假设有线程A,正通过getTask()里获取任务。此时A被中断,在获取任务时,无论是poll()还是take(),都会抛出中断异常。异常被捕获,重新进入下一轮循环,只要队列不为空,就可以继续取任务。
线程A被中断,再次取任务,调用workQueue.poll() or workQueue.take(),不会抛出异常吗?还可以正常取出任务吗?
这就要看workQueue的实现了。workQueue是BlockingQueue类型,以常见的LinkedBlockingQueue和ArrayBlockingQueue为例,加锁时都是调用lockInterruptibly(),是响应中断的。该方法又调用了AQS的acquireInterruptibly(int arg)。
acquireInterruptibly(int arg),无论是在入口处判断中断异常,还是在parkAndCheckInterrupt()方法阻塞,被中断唤醒并判断中断异常时,均使用了Thread.interrupted()。这个方法会返回线程的中断状态,并把中断状态重置!也就是说,线程不再是中断状态了,这
样在再次取任务时,
就不会报错了。
2.2.1.2-2、阶段2 任务刚好要执行完了
这时任务已经快取完了,比如有4条工作线程,只剩下2个任务,那就可能出现2条线程获得任务,2条线程阻塞。
假设A,B获得了任务,C,D阻塞。
A, B接下来的步骤是:
step1.任务执行完成后,再次getTask(),此时符合条件1,返回null,线程准备被回收。
step2.processWorkerExit(Worker w, boolean completedAbruptly) 将线程回收。
阻塞的C,D中的任意一条被中断唤醒后,又会重复step1的动作,周而复始,直到所有阻塞线程都被中断,唤醒。
2.2.2、Shutdown 状态
线程池处在SHUTDOWN状态时,不接收新任务,但能处理已添加的任务,且异步中断闲置的的线程
getTask 返回null, 调用 processWorkerExit(w, completedAbruptly); 移除线程;
当所有的任务已终止,ctl记录的”任务数量”为0,线程池会变为TIDYING状态。接着会执行terminated()函数。
执行完 terminated() 之后,就会由 TIDYING -> TERMINATED,线程池被设置为TERMINATED状态。
2.2.3、Stop 状态
线程池处在STOP状态时,不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
2.3、shutdown 和 shutdownNow 区别 : https://blog.csdn.net/horero/article/details/77622951
四、线程池回收线程源码分析
https://blog.csdn.net/u013256816/article/details/109213183