线程池源码
网上的一些博客对线程池的讲解都是逐行解读源码,看起来可能会比较费力,本文从功能角度出发,以整个流程为切入点,省去一些没必要的源码,带你逐层抽丝剥茧,理解线程池设计的精髓所在。
前置知识
几个常量和变量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private final HashSet<Worker> workers = new HashSet<Worker>();
private final BlockingQueue<Runnable> workQueue;
内部类
Worker简而言之就是对工作线程进一步的封装,不管你是核心线程还是非核心
private final class Worker{
final Thread thread;
Runnable firstTask;
}
线程池的执行过程
通过execute函数我们宏观把握一下整个流程,然后抽丝剥茧逐层递进。这里需要单独说一下addWorker函数,他是用于创建新线程的,他的第二个参数传入一个boolean值,用于区分是核心线程和非核心线程的,后面再深入研究。
下面就是整个流程,没框起来的不用关注 这里还需要提一嘴的是,addWorker就是创建新线程,也就是说我们的线程池是懒初始化的,一开始线程池并没有任何线程,直到有需求过来了才开始逐步创建线程
线程如何在线程池中流转
上面提到,当外界调用线程池的excute函数执行我们的任务时,会通过addWorker函数新建一个线程去执行,那么该线程执行完这个任务之后又何去何从呢?下面进行分析。
其实当我们调用addWorker之后,会执行runWorker函数 因此,我们可以知道,我们的线程在线程池中的一个流转过程大概是这样的:首先线程池的设计是一个懒初始化,当我们提交任务的时候执行任务的线程才会被创建出来,当他执行完这第一个任务之后,他就不断地使用getTask函数尝试去阻塞队列里面拿任务进行执行,如果阻塞队列里面没有任务了,那么这个线程会被阻塞挂起,直到阻塞队列有任务了,会唤醒该线程继续去拿任务执行。而线程的退出我会在下文进行讲解。
非核心线程在空闲一段时间后如何回收
我们知道,当阻塞队列满了,而线程数又没有超过最大线程数的时候,新来的任务会开启新的线程来执行,这些线程称之为非核心线程。而当阻塞队列为空,也就是没有新的任务可以执行,而非核心线程闲置一段时间后,是要被回收的,那么这个回收是如何进行的呢?下面对getTask函数进行详细解读,这里是线程池的一个精髓所在
跟着图中的1,2,3,4的顺序看,这样看起来更容易理解 那么总结一下,关键点在于,getTask是如何从队列中取任务的,当线程数小于等于核心线程数,使用的是阻塞队列的take方法,有任务就取任务,没任务就阻塞。当线程数大于核心线程数,表明有非核心线程参与工作,使用的是阻塞队列的poll方法,传入一个时间参数,有任务直接取任务,没任务的时候不会像take那样一直阻塞等待,而是等keepAliveTime时间后如果还等不到就返回null。而我们知道,如果说getTask返回null,runWorker就不会一直while循环,而是直接退出了,这也就是非核心线程被回收的原理!
如何优雅地关闭线程池——shutdown和shutdownNow
了解关闭线程池之前先了解一下如何关闭一个线程
如何优雅地关闭一个线程
java提供了stop和destroy函数可以强制杀死线程,但是官方是不建议使用了,为什么呢?因为强制杀死线程的话,可能会导致一些资源,比如文件描述符、网络连接等没有正常关闭,浪费系统资源。所以优雅地关闭线程应该是,让线程执行完后自动关闭。
大概可以有两种方式来实现:
1.设置中断标志位
class MyThread extends Thread{
private boolean stopped = false;
@Override
public void run(){
while(!stopped){
...
}
}
}
我们可以通过外界线程来设置stopped标志来让该线程退出,从而结束。那么这样做有一个坏处,当while循环中有阻塞方法,比如wait,那么该线程可能会永远关闭不了。因此,引入第二种方法interrupt
2.interrupt函数
class MyThread extends Thread{
private boolean stopped = false;
@Override
public void run(){
while(!Thread.currentThread().isInterrupted()){
...
}
}
}
然后外界调用该线程interrupt方法就可以结束该程序,与第一个方法不同的是,他可以响应InterruptedException,下面几个函数就是这样:
public static native void sleep() throws InterruptedException;
public final void wait() throws InterruptedException;
public final void join() throws InterruptedException;
线程池状态变化
官方给出的五种线程池状态如下:
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
RUNNING标记为-1,其余都是为正数,从左到右越来越大,不可逆。其中主要是shutdown和shutdownNow两个的区别,这里着重分析一下:
看下面的对比,其实就两处不同,先设置不同的线程状态,然后分别执行各自的中断函数
因此,这里着重分析一下interruptIdleWorkers()和interruptWorkers()
interruptIdleWorkers()
shutdown只是关闭掉那些空闲的线程,阻塞队列里面的任务还是要执行的
interruptWorkers()
shutdownNow()是直接关掉所有线程,管你是不是空闲的,所以代码就直接全部interrupt掉
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers)
w.interruptIfStarted();
} finally {
mainLock.unlock();
}
}
从SHUTDOWN或者STOP状态过度到TIDYING状态
上面的shutdown和shutdownNow函数最后都会执行tryTerminate()方法,这里就不贴代码了,直接口述一下:tryTerminate()不会直接关闭线程池,而是判断一下,当工作线程数为0,工作队列为空的时候,就把线程池的状态从SHUTDOWN或者STOP状态变为TIDYING状态,那么TIDYING状态其实就是我们可能需要执行一些钩子函数,比如在线程池关闭之前我们想关闭一些资源。这是他和TERMINATED状态的唯一区别
点赞是出好博客的绝佳动力
|