Java并发包中的线程池解析

374 阅读10分钟

使用

线程池的使用有很多种不同的方式,JDK中也给我们提供了很多现成的封装好的实现,例如:

Executors.newCachedThreadPool();

但是根据各个开发者的实践,特别是阿里巴巴的Java最佳实践,我们还是应该直接使用ThreadPoolExecutor的构造器来构建满足特定需求的线程池对象。例如,我们可以自己通过代码来实现跟上面相同的缓存特性的线程池对象。

new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                              60L, TimeUnit.SECONDS,
                              new SynchronousQueue<Runnable>());

基础理论

最核心的几个属性

在上述的构建代码中,我们可以看到构造器中需要我们传入好几个参数,而就是这几个参数的不同,就可以构建出满足不同业务需求的线程池对象,下面就先来解释一下这几个参数的含义。

corePoolSize

核心线程的数量。核心线程的意思就是,即便是线程的工作已经结束,进入空闲状态,除非我们另外设置了allowCoreThreadTimeOut为true,那么这些线程对象将一直存在于线程池中。默认allowCoreThreadTimeOut为false,即:核心线程将一直存在于线程池中。

maximumPoolSize

线程池能容纳的线程数量的最大值。

keepAliveTime

这个参数用于设置处于空闲状态的线程经过多久以后会被释放。既包含非核心线程完成工作以后进入空闲状态,也包含核心线程在开启了allowCoreThreadTimeOut参数以后进入空闲状态。

unit

keepAliveTime参数的时间单位

workQueue

工作线程的阻塞队列。这个参数是一个阻塞队列的容器,用于存放还没轮到被执行的任务。这个队列接受的元素的类型就是实现了Runnable接口的示例啦。

threadFactory

用于创建新线程的工厂实例

handler

当用于存储任务的阻塞队列满了之后,应该如何处理新进入的任务的策略实现实例,默认实现为AbortPolicy,直接抛弃任务,并且抛出异常RejectedExecutionException

任务是如何流转的

首先简单的对我们向线程池中提交任务有一个初步的认识,下图就是当我们调用execute方法以后任务的流转过程。

image.png

线程池的内部状态切换

和任务一样,线程池本身也是有状态的,并且会在不同的状态下流转,还是先建立一个直观的印象。下图是一个线程池自身状态的流转图。

image.png

代码解读

线程池的状态是用什么数据结构实现的

线程池的运行是建立在不同的状态切换上的,不同的状态下,对于任务和线程的调度也是不同的,因此搞清楚线程池的状态就理解了一半。下面是和线程池状态相关的主要属性和方法,我们一个一个来解释。

这个ctl就是线程池状态和持有的线程的数量,也就是一个整形记录了2个逻辑信息
如果看框架比较多的话,对这种使用一个整形的高位和低位来存储数据的应该不会陌生
比如Android中的测量参数,MeasureSpec,高2位表示测量模式,低30位保存实际的数值
另外需要注意的是,这里用到了原子类来保证这个参数在多个线程共享中的安全性
初始化的时候,可以看到,设置了线程池的默认状态为 RUNNING,线程数量为0
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

获取当前系统中整形是多少位的,当然整形是4个字节,32位来保存的
这里计算获得的29,意思就是,我们要用低29位来保存线程池中线程的数量
高3位用来保存线程池的状态值
private static final int COUNT_BITS = Integer.SIZE - 3;

这个值就是我们用于获取ctl中存储的线程数量的掩码,二进制的值就是:
00011111 11111111 11111111 11111111
这里像不熟悉微操作的同学解释一下计算的过程:见注释1
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;

二进制表示为:11100000 00000000 00000000 00000000
private static final int RUNNING    = -1 << COUNT_BITS;
二进制表示为:00000000 00000000 00000000 00000000
private static final int SHUTDOWN   =  0 << COUNT_BITS;
二进制表示为:00100000 00000000 00000000 00000000
private static final int STOP       =  1 << COUNT_BITS;
二进制表示为:01000000 00000000 00000000 00000000
private static final int TIDYING    =  2 << COUNT_BITS;
二进制表示为:01100000 00000000 00000000 00000000
private static final int TERMINATED =  3 << COUNT_BITS;

从ctl中分离出高3位表示的线程池的状态码
位的与运算规则为,都是1才是1,否则为0
对COUNT_MASK取反就是:1110000....
所以说,低29位无论传入的是什么值,都会变成0,高3位则传入的数高三位是什么就是什么
private static int runStateOf(int c)     { return c & ~COUNT_MASK; }

从ctl中获得线程数量
private static int workerCountOf(int c)  { return c & COUNT_MASK; }

将rs的高三位和wc的后29位组合成一个新的整形
private static int ctlOf(int rs, int wc) { return rs | wc; }

注释1:

首先是1的二进制表示:00000000 00000000 00000000 00000001

左移29位,那么就是: 00100000 00000000 00000000 00000000

减1,那么就是: 00011111 11111111 11111111 11111111

小结一下: 线程池使用了一个线程安全的整形来保存当前状态和工作线程数量,高3位位状态,后29位位工作线程数量,默认线程池处于RUNNING状态,工作线程数量为0。除了RUNNING状态,其他状态都是非负数。

execute方法

这个方法官方是自带了说明的,描述了往线程池中添加新任务需要处理的3种情形,原理我们上面已经用流程图说的比较清楚了。

public void execute(Runnable command) {
    // 健壮性检查,保证传入的任务不为空
    if (command == null)
        throw new NullPointerException();
        
    // 因为ctl是一个原子类,因此需要使用get方法获取其中的值
    int c = ctl.get();
    
    // 如果当前线程池中的工作线程数量还没到达核心线程的数量
    if (workerCountOf(c) < corePoolSize) {
        // 直接添加一个核心线程来执行任务,addWorker的第二个参数的含义就是是否创建的是核心线程
        if (addWorker(command, true))
            创建成功的话那么就结束这个流程了
            return;
            
        // 如果创建线程失败,那么要重新更新一下当前的ctl值,因为这期间可能别的线程也会有修改行为
        c = ctl.get();
    }
    
    // 走到这里说明当前线程池中的工作线程数量已经超过核心线程数量了
    // 判断当前是否可以往阻塞队列中添加这个任务,线程池状态必须是RUNNING状态
    if (isRunning(c) && workQueue.offer(command)) {
        // 能进来说明任务已经被成功添加进阻塞队列了,等待被执行
        // 获取ctl的最新值
        int recheck = ctl.get();
        // 如果线程池的状态已经不是RUNNING,那么我们就不能让这个任务进入队列或者被执行了
        // 因此要先从队列中移除该任务
        if (! isRunning(recheck) && remove(command))
            // 然后调用拒绝策略
            reject(command);
        // 如果当前依然是RUNNING状态,但是工作线程已经没有了
        else if (workerCountOf(recheck) == 0)
            // 能进来的意思是,我们成功将传入的任务放进了阻塞队列中,
            // 但是乜有线程能执行了,
            // 那么我们就需要创建一个空任务的非核心线程来执行队列中的任务
            addWorker(null, false);
    }
    // 到这里代表当前工作线程数量已经超过核心线程数量,并且阻塞队列也插入失败,
    // 一般就是队列满了,那么我们就要尝试创建非核心线程来执行当前任务
    else if (!addWorker(command, false))
        // 如果非核心线程也不允许创建了,那么就直接走拒绝策略了
        reject(command);
}

小结一下: 可以看到,execute方法的逻辑非常清晰,紧密的围绕我们在构建线程池的时候构建的那几个参数展开。先判断核心线程数量是否还足够,如果核心线程数量还有富余,那么直接创建核心线程并且执行当前任务。如果核心线程数量已经满了,那么就会先看阻塞队列还有没有位置,如果有位置的话,那么就会把任务存进阻塞队列中,等待被执行。如果阻塞队列也满了,那么就检查能不能创建非核心线程来执行任务。如果还是不行,那么就走拒绝策略了。 引申一下: 从上面的分析我们可以看到,非核心线程的优先级是在阻塞队列之后的,但是假设我们希望提升执行效率,缩短任务执行时间,优先使用非核心线程来处理任务呢?根据源码,我们只有控制工作队列的offer方法才能达到这个目的。因此,一个可行的方案就是继承使用的阻塞队列,重写offer方法,在某些情形下让该方法返回false,也就是不让任务入队,而优先走创建非核心线程的分支。

addWorker

这个方法很长,我们分成2部分,依次来看看这个方法做了一些什么操作。这个方法有2个参数,第一个参数为我们需要被执行的任务,第二个代表我们希望是核心线程来执行还是非核心线程。当然,对于真正执行任务的线程对象来说,并没有什么区别,他们唯一的区别就是存活时间长短的差异。

private boolean addWorker(Runnable firstTask, boolean core) {
    // 这是一个很有趣的特性,这是java的代码tag,你其实可以使用任何的单词作为tag
    // 然后在循环中,break或者continue就可以直接直接跳转到这些标签的位置
    // 类似goto语句
    retry:
    // 开启一个无限循环,给临时变量c复制为最新的ctl的值
    // 无限让自己循环,直到操作成功,这就是很典型的自旋锁保证线程安全执行的方式
    for (int c = ctl.get();;) {
        // 线程池的状态至少要是SHUTDOWN,也就是可以是:除了RUNNING以外的状态
        // 也就是说当前线程池已经没有处于运行中状态了,那么按照设计,此时就
        // 不应该再接收新任务了
        
        // 这里我们分情况来讨论,如果当前是SHUTDOWN之后的状态,那么就直接返回false
        // 如果当前是SHUTDOWN状态,虽然不接收新任务,但是我们要继续让线程池处理队列中的任务
        // 因此要跳过这个return的方式只有,传入的任务为空或者队列不为空,还记得上一部分
        // 我们发现没有线程了,但是队列中海油任务待处理的场景吗,就是这里要刨除的场景
        // 好让我们创建一个非核心线程来接着处理队列中的任务
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP)
                || firstTask != null
                || workQueue.isEmpty()))
            return false;

        for (;;) {
            // 这里就是判断当前线程池中的工作线程数量是否已经超过最大限度
            if (workerCountOf(c)
                >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                // 如果已经超过了,那么就不要再添加了
                return false;
            // 使用CAS来新增线程池中的线程数量
            if (compareAndIncrementWorkerCount(c))
                // 如果更新成功,那么就跳出循环,回到这个方法打retry标记的地方
                break retry;
            // 走到这里,说明线程数量未满但是更新数量失败了
            // 更新ctl最新的数据给局部变量c
            c = ctl.get();
            // 如果当前状态已经是非运行中状态
            if (runStateAtLeast(c, SHUTDOWN))
                // 那么就跳转到retry标签,也就是代码的最开始除重新运行
                continue retry;
            // 如果当前依然是RUNNING状态,那么就回到内部循环出再次更新工作线程数量
        }
    }
}

小结一下: 前半部分说白了就是在尝试新增一个工作线程的数量,那么就需要线程池满足2个条件。第一,处于运行中,即:RUNNING状态;第二,线程池中的工作线程数量还没满。

private boolean addWorker(Runnable firstTask, boolean core) {
    // ......省略上一部分
    // 上一部分我们主要是决定要不要开新线程来执行新加入的任务
    // 那么接下来,就代表确实需要新开线程来执行任务了,下面就是具体开启的方法
    
    // 工作线程是否被启动
    boolean workerStarted = false;
    // 工作线程是否已经被添加
    boolean workerAdded = false;
    // 工作线程封装对象,简单的介绍一下这个类
    // Worker类继承自AQS,实现了Runnable接口,因此主要就是作为Runnable增强功能
    // 继承AQS是为了通过AQS中的state来更方便的响应锁和中断
    // 在new Worker的时候,会默认将Worker父类AQS中的state设置为-1,
    // 线程开始运行以后就设置为0
    Worker w = null;
    try {
        // 构建一个工作线程对象
        w = new Worker(firstTask);
        // 获得真正的线程对象
        final Thread t = w.thread;
        if (t != null) {
            // 构建一个可重入锁对象
            final ReentrantLock mainLock = this.mainLock;
            // 获取可执行凭证,获取不到就会自旋或者被挂起
            mainLock.lock();
            try {
                // 再次获取ctl的最新值,以防被别的线程更新过了
                int c = ctl.get();

                // 判断条件有2个
                // 1. 线程池状态是否处于运行中状态
                // 2. 运行状态为SHUTDOWN并且传入的任务为空
                // 还记得上面我们见过的那个addWorker(null, false)的特殊处理吗?
                if (isRunning(c) ||
                    (runStateLessThan(c, STOP) && firstTask == null)) {
                    // 进来就代表线程池状态是支持我们新开工作线程的
                    // 判断新创建的线程对象的状态是否是初始状态,不是的话就抛出异常
                    if (t.getState() != Thread.State.NEW)
                        throw new IllegalThreadStateException();
                        
                    // 添加到工作队列的集合中
                    // workers是一个HashSet,保证工作线程的唯一性
                    workers.add(w);
                    // 记录工作线程已经被添加成功
                    workerAdded = true;
                    // 获取当前工作线程的数量
                    int s = workers.size();
                    // 更新线程池拥有过的最多的线程数量
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                }
            } finally {
                // 释放锁
                mainLock.unlock();
            }
            // 如果添加工作线程成功
            if (workerAdded) {
                // 启动线程,传入的Runnable就会被执行
                t.start();
                // 记录工作线程被成功启动
                workerStarted = true;
            }
        }
    } finally {
        // 如果没有添加工作线程成功
        if (! workerStarted)
            // 那么就要回滚添加工作线程的操作,这个方法很重要,我们下面会单独解释
            addWorkerFailed(w);
    }
    // 返回工作线程有没有被成功启动
    return workerStarted;
}

// Worker类的构造器
Worker(Runnable firstTask) {
    // 设置为-1表示默认让线程处于阻塞状态,唤醒以后将被设置为0
    setState(-1);
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

// 回滚添加工作线程失败的所有操作
private void addWorkerFailed(Worker w) {
    // 获取线程操作锁
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        if (w != null)
            // 从工作线程集合中移除该对象
            workers.remove(w);
        // 给当前的线程数量-1
        decrementWorkerCount();
        // 尝试将线程池切入TIDYING状态,然后结束线程池
        tryTerminate();
    } finally {
        // 释放锁
        mainLock.unlock();
    }
}

final void tryTerminate() {
    // 自旋保证线程任务被执行
    for (;;) {
        // 获取最新的ctl的值
        int c = ctl.get();
        // 这里判断的是不需要进入TIDYING状态的情况
        // 如果当前线程池的状态还在运行中
        // 或者已经是TIDYING及之后的状态了
        // 或者是SHUTDOWN状态并且阻塞队列中还有任务没处理完,我们知道线程池的设计要求
        // 处于SHUTDOWN状态下只是不接收新任务,但对于已经处于队列中的任务要继续执行的
        if (isRunning(c) ||
            runStateAtLeast(c, TIDYING) ||
            (runStateLessThan(c, STOP) && ! workQueue.isEmpty()))
            return;
        // 走到这里就代表当前已经满足进入TERMINATED状态的条件
        // 判断工作线程的数量是不是不为0
        if (workerCountOf(c) != 0) {
            // 中断一个工作线程
            interruptIdleWorkers(ONLY_ONE);
            return;
        }

        // 走到这里就代表线程池中的工作线程数量也没有了,那么按照设计,就可以正式
        // 终止线程池了,进入TERMINATED状态
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            // 将线程池的状态转换为TIDYING状态,代表线程池的清理要开始了
            if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                try {
                    // 需要由子类来实现的回调,默认为空实现
                    terminated();
                } finally {
                    // 将线程池状态过渡到TERMINATED状态,表示线程池被终结了
                    ctl.set(ctlOf(TERMINATED, 0));
                    // 释放清理相关的线程锁
                    termination.signalAll();
                }
                return;
            }
        } finally {
            mainLock.unlock();
        }
        // 如果操作失败了,那么就回到循环的开始处,再来重试一次
    }
}

小结一下:addWorker方法的第二阶段,我们需要尝试创建一个工作线程来处理任务,而这个工作线程会被封装在Worker类中,这个类继承自AQS,可以通过state参数来控制是否中断绑定的线程。工作线程创建成功以后就会立刻开始执行,并且添加到数据结构为HashSetworkers集合中。如果线程运行失败,那么就说明可能当前线程池已经进入了关闭阶段,就会调用tryTerminate方法尝试终止线程池。

至此,添加工作线程来处理新任务的流程我们已经讲清楚了。

总结

至此,我们就对线程池的实现逻辑从源代码层面有了一个比较清晰的认知。

  1. 线程池的内部状态由一个AtomicInteger来存储,保证线程的安全性。其中的高3位存储线程池的状态,低29位存储工作线程的数量。运行中RUNNING状态为-1,别的跟停止相关的状态都是非负数。
  2. 线程池初始状态为RUNNING。
  3. 当有新任务进来时,首先判断是否还有核心线程的坑位,如果有,那么就直接创建核心线程来执行任务。如果核心线程满了,那么就先看看阻塞队列是否有坑位,有的话就放进阻塞队列中,等待被执行。如果阻塞队列也满了,那么就看看非核心线程还有没有坑位,有的话就创建非核心线程来执行任务。最后都不行,那就只能走拒绝策略了。
  4. 默认的拒绝策略是AbortPolicy,抛出异常。