第二十一章 对ThreadPoolExecutor源码的理解

前言

最近一段时间都没有空闲阅读和学习Java基础核心知识,主要是最近换了工作,然后忙完工作,又忙结婚,十月份刚刚完成人生大事,以为终于可以歇段时间,结果小bybe又来了,看来没空歇了。既然没空歇了,只有在日常间歇挤出时间学习了。

下面本章将会分析Java的线程池源码,以及一些原理分析,如果有不正确的地方请指正。先看线程池常用的家族体系。
在这里插入图片描述

在常用家族体系结构中,Executor是线程池顶层接口,ExecutorService也是接口。我们常用的线程池实体类是ThreadPoolExecutor,ScheduledThreadPoolExecutor还有Executors这个工具类。Executors提供了很多创建线程池的方法,但其内部都是基于ThreadPoolExecutor来实现的。还有ScheduledThreadPoolExecutor是一种定时线程池,它也是继承于ThreadPoolExecutor,所以对于研究线程池的人来说,ThreadPoolExecutor的研究分析是其重中之重。

ThreadPoolExecutor 序

它像一台庞大的机器,但又不好在描述具体图像,于是我把它比喻为一艘航空母舰。
航空母舰的特点是非常大,组成结构很复杂,运行机制繁琐,都具有平台作用。ThreadPollExecutor就和航空母舰一样,作为一个载体,它可以装载一些战斗机,每个战斗机都可以去执行不同的任务,不同的时候它也对应着很多种状态。
这就是ThreadPoolExecutor这艘航空母舰的大概描述,下面将分为结构组成、工作原理、实现机制来做一个大概的剖析。

一、 结构组成

ThreadPollExecutor的骨架由六大部分构成。

核心线程数、最大线程数、线程存活时间、任务队列、线程工厂、拒绝策略。
在这里插入图片描述
是不是像个机器人? 有模有样了,一个大机器的形状,但是每个部位怎么设计才能满足需要,需要设计各个部位的参数。
那么每个参数有什么意义呢?参数之间又有什么关联呢?

故事背景: 
近年来,索马里海盗猖獗,许多的商货运输轮船都受到劫持,部分轮船还有人员伤亡,为此某部队准备派遣一艘航空母舰前往打击这些海盗。

1. 任务队列
workQueue:存放任务的队列。

所有关于海盗的任务由总部收集信息后,发向航空母舰。航母可以设置一个任务接收队列。BlockingQueue 是一个阻塞队列的顶层接口,其实现有很多种,常见的几种队列:有界队列、无界队列、延迟队列、同步队列等等。

(1)有界队列代表:ArrayBlockingQueue 、 LinkedBlockingQueue
这种队列的特点有一定的长度,ArrayBlockingQueue 底层由数组实现,必须指定大小。LinkedBlockingQueue默认大小为Integer.MAX_VALUE,他们是一个有固定界限的任务队列。

(2)无界队列代表:PriorityBlockingQueue
这种队列的特点是没有界限,没有容量标志。

设计好了任务系统后,下一步就是飞机了。
飞机是航母上的核心,那么看一下飞机怎么生产出来的,即线程池中的线程。

2. 线程工厂
threadFactory: 创造线程的工厂。它是线程池中的一个变量。ThreadFactory也是一个接口,定义了唯一的顶层方法newThread()。
要配置这个线程工厂可以有两种做法:
(1) 采用常用的线程工厂实现类DefaultThreadFactory。
(2) 采用自己定义的实现类,可以更加个性化的满足业务中的需要。

飞机生产工厂有了,但是不可能让工厂无限制的生产,毕竟每架飞机耗费的资金巨大。所以需要根据业务的需要确定生产个数。
由于通常情况下,任务的数量是浮动的,所以生产的个数需要由两个参数来控制,即核心线程数和最大线程数。

3. 核心线程数
corePoolSize: 代表初始核心工作线程的数量。

在平时,航母上只装载了几架飞机,虽然索马里的海盗猖獗,但是好在不多,派出的飞机足以将他们打击。
但是临近过年了,平时不做海盗的平民也来当海盗了,越来越多的海盗出现在了索马里。航母上的飞机一下子忙不过来了。
这时候开始向总部请求支援飞机,但是最多可以支援多少呢,这取决于航母设计时甲板的容纳量。

4. 最大线程数
maximumPoolSize: 代表可以容纳包括核心线程在内的最大工作线程的数量。
触发条件: 核心线程用完,并且任务队列已满。

增加了飞机后,有两个可能性。
(1)战斗力增强后,所有海盗都能够被打击了。
(2)即使甲板的飞机容量已经达到上限了,海盗们还是打击不过来。

按照上述第一个可能性,增派了飞机,海盗都能够被打击了,工作又恢复到平日里的样子。随着过年那段时间一过,平日里海盗又没有那么多了。
本该高兴的事情,舰长开始愁了,后面增派飞机后,每个月都要增加保养、加油、训练,还得让食堂多准备些馒头,这些飞行员真能吃,一口气吃仨个。
所以舰长想了想得让那些待业的飞机回去。

5. 线程存活时间
keepAliveTime:线程空闲后进行回收的时长。
unit: 这个是时间单位。配合keepAliveTime组成一个时间。

这个参数是舰长想出来的一个损招,发出了一个全员通告:任何飞机,如果没有那么多海盗了,待机满XX天后必须返回总部。
这是发给所有飞机的一个共同的通告,但是为了维持日常任务需要,还是会留下核心数量多的飞机,这样食堂的馒头也不用抢了,舰长打着自己的小算盘。
但是既然是发给所有飞机的通告,那么当索马里没有海盗了,核心数量多的飞机需要回去吗?这取决于allowCoreThreadTimeOut这个参数是否为true。

6. 拒绝策略
handler: 线程池满了以后的拒绝策略。
触发条件:线程使用完了,并且任务队列也满了才会触发。所以当在使用无界阻塞队列的情况下,拒绝策略几乎不会被触发。

按照刚刚的第二种可能性,如果所有飞机都派完了,任务队列也装满了,总部不断发出的任务怎么办?

拒绝策略有四种,分别是AbortPolicy、DiscardPolicy、DiscardOldestPolicy、CallerRunsPolicy。它们都实现了RejectedExecutionHandler接口,并实现了各自的rejectedExecution()方法,每个拒绝策略的该方法实现就是其具体的拒绝实现细节。

AbortPolicy:线程池默认的一种拒绝策略。它表示总是抛出异常。
DiscardPolicy:它的rejectedExecution()方法什么也没做,所以会直接导致任务丢弃。
CallerRunsPolicy:这种拒绝策略在线程未关闭的情况下会直接在调用者的线程中执行任务,如果线程被关闭了,如同DiscardPolicy导致任务丢弃。
DiscardOldestPolicy:最后一种拒绝策略它表示,如果线程池未关闭,则会将新任务添加到任务队列尾部,同时会排挤掉队列首部的任务。如果线程池已关闭,则如同DiscardPolicy。

二、 工作原理

根据上述参数的特性,在此将线程池的运行情况分为以下几个部分,并引入例子,方便理解:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
										   (corePoolSize)5,
											(maximumPoolSize) 20,
											(keepAliveTime) 30,
											(unit) TimeUnit.SECONDS,
											(workQueue) new LinkedBlockingDeque<>(),
											(threadFactory) Executors.defaultThreadFactory(),)

(1)刚刚开机,没有任务
刚刚开机,即刚刚创建好线程池的时候,此时线程池内没有线程被创建。只是一个空池子,此时处于待命状态。
在这里插入图片描述

有些人认为线程池创好以后,核心线程池也会立即被创建出来了。其实不是,核心线程什么时候被创建呢,下一步会具体分析。
(2)同时接收到的任务数量 <= 核心线程数[5]。
当第一个任务传来时,由于此前线程池是空的,所以会利用线程工厂立即创建一个线来执行此任务。

思考? 按上面的理论,当第二个任务传来时,也会立即创建第二个线程吗?答:非也!

如果第二个任务来临时,第一个线程还在执行,线程池则会接着创建第二个线程,如果第一个线程是空闲的,则还是利用第一个线程来执行该任务。
后面的线程同理,在满足本条件范围内,都是能复用则复用,不能复用时才创建新的核心线程。

如果任务数量继续增加时,请看下一步分析。

(3)同时接收到的任务数量 > 核心线程数时,分a、b、c、d四种情况。
a .任务数量 <= (核心线程数[5] + 任务队列长度[2^31 - 1])
此情况下,由于核心线程都在运行中,后面的任务则会被添加到任务阻塞队列。
如果有一些核心线程运行完任务了,会继续去队列中取出任务,所以队列中取任务的操作可能是一个多线程并发行为,为了保证线程安全性,采用阻塞队列而不是传统队列。

b. 任务数量 > (核心线程数[5] + 任务队列长度[2^31 - 1])
随着任务突然的爆发性增长,队列已经被装满了,无法满足任务增长的需求。
此时,线程池会创建临时线程,以满足任务需要。

思考:此时的临时线程执行的任务是队列中poll出来的任务,还是任务队列装满后多余出来的任务?

这个问题可以在方法execute(Runnable command)中找到,此时临时线程是执行这个无法被添加到队列去的任务,属于多余出来的任务。而只要队列中又被消费了一个任务,后续任务又会优先添加到队列中去。

c. 任务数量 <= (最大线程数[20] + 任务队列长度[2^31 - 1])
线程池中的线程会按照b的策略,如果需要临时线程,线程池会不断创建出这些临时线程。创建的最大线程个数取决于参数maximumPoolSize。
而一旦发现超过了这个参数。就会发生下面d的情况。

d. 任务数量 > (最大线程数[20] + 任务队列长度[2^31 - 1])
如果此时队列已装满,线程个数已达到最大数量。线程池为该情况提供了四种拒绝策略。调用者可以通过设置拒绝策略和判断拒绝策略来实现业务要求。

(4)线程回收机制
核心线程和临时线程的回收机制是一样的。回收机制有两方面来决定。

  1. 是否进行回收
  2. 多久后进行回收

针对第一个问题,线程池规定临时线程是必须要回收的。而核心线程是根据用户设置的allowCoreThreadTimeOut值来确定是否回收。
针对第二个问题,核心线程和临时线程的回收时长都是一样的,有keepAliveTime + unit来决定的。

三、 实现机制

线程池看似复杂,其实内部很简单。只要弄清楚了上述线程池运行原理,带着原理去理解源代码,犹如轻车熟路。
首先来看一下内部成员变量,我将其分为常规性变量和特殊变量。

变量

常规性变量:

private final BlockingQueue<Runnable> workQueue; 				// 任务队列
private final ReentrantLock mainLock = new ReentrantLock();		// 线程池的主锁
private final Condition termination = mainLock.newCondition();	// 配合线程池主锁的释放器
private final HashSet<Worker> workers = new HashSet<Worker>();	// 线程的存储容器 
private int largestPoolSize;									// 最大线程数量
private long completedTaskCount;								// 线程池以及执行完成的任务个数
private volatile ThreadFactory threadFactory;					// 线程池的线程工厂
private volatile RejectedExecutionHandler handler;				// 拒绝策略
private volatile long keepAliveTime;							// 线程空闲后的回收时间限制
private volatile boolean allowCoreThreadTimeOut; 				// 是否允许核心线程超时后进行回收
private volatile int corePoolSize;								// 线程池的核心线程数量
private volatile int maximumPoolSize;							// 线程池的最大线程数量
private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();// 默认的拒绝策略

特殊变量

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); // 一个原子类的CAS操作
private static final int COUNT_BITS = Integer.SIZE - 3;					// COUNT_BITS 为29,表示int类型二进制中,其中29位用来表示线程池中线程的最大运行数量,剩余3位用来表示线程池状态。
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;			// 线程池中的线程最大容量
private static final int RUNNING    = -1 << COUNT_BITS;		// 运行态,二进制111 00000000000000000000000000000
private static final int SHUTDOWN   =  0 << COUNT_BITS;		// 关闭态,二进制000 00000000000000000000000000000
private static final int STOP       =  1 << COUNT_BITS;		// 停止态,二进制001 00000000000000000000000000000
private static final int TIDYING    =  2 << COUNT_BITS;  	// 整理态,二进制010 00000000000000000000000000000
private static final int TERMINATED =  3 << COUNT_BITS; 	// 结束态,二进制011 00000000000000000000000000000 

关于特殊变量的设计思路,从上面也许已经看出了一些端倪。

  1. 设计者是想用int类型的值,来表示当前线程池的运行状态和线程运行数量两个数值。其中int类型的32位字节中,前3位用来表示状态,后29位用来表示数量。
  2. ctl变量就是这样一个集成的原子类变量,并且从
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    可以看出,线程池创建后,默认是RUNNING状态,线程数量为0。这也解释了上述原理中为什么线程池刚刚创建后,并没有将核心线程数创建出来,而是一个空线程池的原因。
  3. 从状态值的设计来看,RUNNING状态是最小,比它大的都是非RUNNING状态。所有后面源码中有些地方就是通过RUNNING态和非RUNNING态来做判断。

构造器

构造器就是对变量简单的赋值,并且有几个重载。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

Worker(内部类)

这个是线程池的核心,我们理解为工人,集成了Thread 和 Runnable。
所以它可以用获得的线程执行接收到的任务。

final Thread thread;
Runnable firstTask;
volatile long completedTasks;

构造一个Worker是需要一个任务的,所以这个任务是它需要执行的第一个任务。

Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);		// 利用线程工厂创建。
    }

有了任务,并且也有了线程。那么Worker就可以用线程执行任务了,如何执行呢?

public void run() {
        runWorker(this);
    }
    
官方注释:
主工作程序运行循环。从队列中反复获取任务并执行它们,同时处理一些问题:
1. 我们可以从一个初始任务开始,在这种情况下,我们不需要获得第一个任务。否则,只要池在运行,我们就会从getTask获得任务。如果返回null,则工作程序将由于池状态或配置参数的更改而退出。其他退出是由于抛出外部代码中的异常导致的,在这种情况下complete突然保持,这通常会导致processWorkerExit替换此线程。
2.在运行任何任务之前,会获得锁,以防止任务执行时其他池中断,然后我们确保除非池停止,否则这个线程不会设置它的中断。
3.在每个任务运行之前,都会调用beforeExecute,这可能会抛出一个异常,在这种情况下,我们会导致线程在不处理任务的情况下死亡(用completedsuddenly true中断循环)。
4.假设beforeExecute正常完成,我们运行任务,收集它抛出的任何异常发送到afterExecute。我们分别处理RuntimeException、Error(规范保证会捕获这两者)和任意可抛掷事件。因为我们不能在Runnable.run中重新抛出可抛弃物,所以我们在抛出时将它们包装在错误中(到线程的UncaughtExceptionHandler中)。保守地说,抛出的任何异常都会导致线程死亡。
5.在task.run完成后,我们调用afterExecute,这也会抛出一个异常,这也会导致线程死亡。根据JLS第14.20节,即使task.run抛出,这个异常也会生效。异常机制的最终效果是,在执行后,线程的UncaughtExceptionHandler提供了我们所能提供的关于用户代码遇到的任何问题的同样准确的信息。

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}


详细的源码就不分析了吧,后面再来。
这里分析主要的API。

execute方法

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * 分3个步骤进行:
     *
     * 1.如果运行的线程少于corePoolSize,则尝试使用给定命令作为其
     * 第一个任务启动一个新线程。对addWorker的调用会自动检查runState和
     * workerCount,从而通过返回false防止在不应该添加线程的情况下添加线程的错误警报。
     *
     * 2.如果任务可以成功排队,那么我们仍然需要再次检查是否应该添加一个线程
     * (因为现有的线程在上次检查后死亡),或者池在进入此方法后关闭。因此,我们会
     * 重新检查状态,如果停止队列,必要时回滚队列;如果没有线程,则启动一个新线程。
     *
     * 3. 如果它失败了,我们知道我们被关闭或饱和,因此拒绝这个任务。
     */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

submit方法

ThreadPoolExecutor的submit方法是继承自父类。

public <T> Future<T> submit(Callable<T> task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<T> ftask = newTaskFor(task);
    execute(ftask);
    return ftask;
}

从内部可以看出,submit还是调用的execute方法。与execute不同的是submit会返回一个Future; 通过Future可以操作任务。

submit 与 execute的不同

猜你喜欢

转载自blog.csdn.net/weixin_43901067/article/details/108033415