浅谈ThreadPoolExecutor线程池的原理

使用方法

这里我们用最简单的形式来创建一个线程池,目的是先演示一下使用ThreadPoolExecutor的使用方法

    public static void main(String[] args) {
    	// 创建线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 10, 200, TimeUnit.SECONDS, new LinkedBlockingQueue<>(5));

        for (int i = 0; i < 10; i++) {
        	// 使用线程池
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread() + " -> run");
                }
            };
        }
        // 销毁线程池
        threadPool.shutdown();
    }
复制代码

就这么简单,一个线程池就演示完毕了,包含了创建、使用和销毁的全过程

概念讲解

为了接下来的源码分析做准备,这里先来详解一些与ThreadPoolExecutor线程池有关的一些概念

参数

ThreadPoolExecutor的构造方法中需要传入很多参数,这些参数的含义也十分重要,希望大家能记住,下面按顺序列出构造方法中的参数:

  • corePoolSize:核心线程数,在不主动设置的情况下,核心线程是不会自动销毁的,超过了核心线程数的线程会根据设置的等待时间自动销毁
  • maximumPoolSize:最大线程数,线程池可容纳的最大线程数
  • keepAliveTime:空闲线程(非核心线程)在等待keepAliveTime时间后会自动销毁
  • unit:keepAliveTime的时间单位
  • workQueue:任务队列,如果线程数量超过核心线程数,则新来的任务会先进入这个队列中,然后再做下一步判断,用于缓冲
  • threadFactory(可选):线程工厂,用于设定创建线程的方式,一般用来设置线程名
  • handler(可选):拒绝策略,当队列满或线程池满时,类中提供了四种拒绝策略

里面可讲的不多,值得一提的是workQueue是一个BlockingQueue类型的对象,一般选用ArrayBlockingQueue或LinkedBlockingQueue

还有就是handler在ThreadPoolExecutor中有以下四种可选参数:

  • AbortPolicy(默认):丢弃任务并抛出异常
  • CallerRunsPolicy:让调用execute方法的线程来执行该任务
  • DiscardPolicy:丢弃任务,不抛出异常
  • DiscardOldestPolicy:丢弃队列中等待中最久的任务(队列头的任务),然后将该任务加入队列
线程池状态

ThreadPoolExecutor类中有一个ctl变量,我喜欢叫它线程池标志码,是一个32位整数,高3位用于标志线程池的状态,剩下29位表示池中的当前线程数

关于这个高三位,有以下几种状态标识:

  • -1:RUNNING,运行状态,表示线程池正常工作
  • 0:SHUTDOWN,关闭状态,表示线程池工作准备结束,不能接受新任务,但可以处理在任务队列中排队的任务
  • 1:STOP,停止状态,表示线程池停止工作,不能接受新任务,也不能处理排队中任务,同时会中断执行中的任务
  • 2:TIDYING,收工状态,表示线程池即将终结,所有任务都已终止,工作线程数为0,会马上运行terminated()钩子方法结束线程池的生命周期
  • 3:TERMINATED,终结状态,表示线程池寿命结束,terminated()方法调用结束

对这些状态之间的转换方式有兴趣的也可以了解一下:

  • RUNNING -> SHUTDOWN:调用shutdown方法后
  • (RUNNING or SHUTDOWN) -> STOP:调用shutdownNow方法后
  • SHUTDOWN -> TIDYING:当队列和池中的工作线程均为空时
  • STOP -> TIDYING:terminated方法执行完毕后
执行策略

关于一个任务提交线程池之后,将执行什么样的行为,以下步骤应该能说得很清晰了:

  1. 提交一个任务到线程池中
  2. 判断池中的工作线程数是否小于核心线程数,如果小于,就添加一个线程,然后执行该任务
  3. 判断队列是否已满,如果已满,就进入第5步,否则将其添加到队列中
  4. 从队列中取出该任务,如果移除失败,就调用拒绝策略来处理
  5. 判断线程池是否已满,如果已满,调用拒绝策略来处理,否则进入下一步
  6. 创建一个新线程到线程池中执行该任务

看完这部分可能你印象不深,甚至根本就不理解,没关系,我们接下来就要从源码来分析这些步骤到底是怎么实现的

源码讲解

execute()

与框架源码比起来,JDK源码显得格外亲民,我们的关注点只需放在一个方法上即可,这里直接进入execute方法中,这个方法是整个线程池的核心方法,读懂了这个方法就读懂了线程池的工作流程,其源码如下:

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
            
        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);
    }
复制代码

先别急着看,这里我先来解释一下其中出现的一些可能有些不太好懂的方法的含义,等到后面再来抽几个重要的方法讲解一下:

  • workerCountOf:获取池中的当前线程数
  • isRunning: 判断是否线程池在正常运行状态
  • addWorker:添加一个任务到线程池

好了,根据提供的这些方法,我们打上详细的注释,再来回头看一遍这个方法:

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        // 获取线程池标识码
        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);
    }
复制代码

和我们在上面总结的完全一致

addWorker(Runable, Boolean)

如果你仅仅希望了解一个执行流程,那本篇文章对你来说已经结束了,否则的话,现在才到了真正的重头戏,我们来看源码:

    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            // 获取状态码
            int rs = runStateOf(c);

			// 如果线程池不在运行状态,且以下状态不成立:
			// 		线程池已经调用shutdown()停止运行,同时要添加的任务为空但任务队列不为空
			// 就不进行任务的添加操作,直接返回false
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                // 返回添加失败
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                // 如果core为true,就判断是否超过核心线程数,否则就判断是否超过最大线程数
                // 如果超过了限制就直接返回false
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                // 通过死循环,不断尝试自增工作线程的数量
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                // 自增成功,重新获取一下线程标识码的值
                c = ctl.get();
                // 如果线程状态发生改变,就返回重试
                if (runStateOf(c) != rs)
                    continue retry;
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        // Worker实现了Runnable接口,是经过封装后的任务类
        Worker w = null;
        try {
        	// 将任务封装为Worker对象,
        	// 同时会调用线程工厂的newThread方法来生成一个执行该任务的线程
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                // 以下为同步方法,需要使用Lock加锁
                mainLock.lock();
                try {
                	// 获取线程池状态
                    int rs = runStateOf(ctl.get());

					// 如果运行状态正常,
					// 或虽然调用shutDown()方法停止线程池,但是待添加任务为空
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        // 如果线程已经启动,就表示不能再重新启动了,则抛出异常
                        if (t.isAlive())
                            throw new IllegalThreadStateException();
                        // 将该线程添加到workers中
                        // workers包含所有的工作线程,必须工作在同步代码块中
                        workers.add(w);
                        int s = workers.size();
                        // largestPoolSize是整个线程池曾达到过的最大容量
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                	// 解锁操作必须写在finally语句中,因为Lock在抛出异常时不会自动解锁
                    mainLock.unlock();
                }
                // 如果添加任务成功,就启动线程
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
        	// 不管怎么样,只要启动失败,就会进行操作的回滚
            if (! workerStarted)
                addWorkerFailed(w);
        }
        // 返回是否启动成功,而不是是否添加任务成功
        return workerStarted;
    }
复制代码

我把整个方法分为四部分:

  1. 预检查:保证线程池和任务队列能正常进行操作
  2. 预操作:提前自增工作线程的个数,通过自旋锁保证同步
  3. 添加任务:将待执行的任务封装成为Worker对象,添加到任务集合中
  4. 执行任务以及异常回滚:启动工作线程,如果启动失败会对之前的操作进行回滚

如果有看不懂的地方,配合代码中的注释应该也能差不多看懂,如果只是扫一眼我的总结就抱怨看不懂,那还是建议另寻别处,既然我这里把注释完整地补上了,还是希望大家能认真看完,这里的总结仅仅是一个简单的概括

这里有一点难以理解的就是预检查部分的一段代码,就是下面这段:

            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;
复制代码

我这里再详细讲一下,首先,这里判断了两个条件,如果第一个条件满足且第二个条件不满足时,会直接返回false,也就是直接失败。我们反过来想,什么情况下会继续执行?这么一来条件就简化了很多,也就是以下条件成立时,会接着进行判断,而不会直接返回失败:

  • 线程池处于正常运行状态
  • 线程池处于关闭状态,且待添加的任务为空,并且任务队列不为空(此时会处理排队任务)

总结

这样整个线程池的原理就讲完了,在总结部分我也不准备再重复一遍流程了,因为流程已经都在上面的概念部分概括了,我这里就说几点要注意的吧:

  • 如果你不手动传入线程工厂,线程池会提供一个默认的线程工厂,而不是简单的new Thread(Runnable)创建线程
  • 线程池调用shutdown()方法后,一定记住线程池并不会立马关闭,而是会接着处理排队任务
  • ...等以后想到了再补充

猜你喜欢

转载自juejin.im/post/5cef621a51882520724c7c01