ThreadPoolExecutor源码分析

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

梳理完JDK线程池的继承体系,相信大家对线程池的上下关系、发展脉络已经有了初步认识。但实际工作中,Executor、ExecutorService、AbstractExecutorService并不常见,我们一般直接使用ThreadPoolExecutor这个实现类。通常情况下,大家讨论线程池时,其实都是在讨论ThreadPoolExecutor,只不过他们自己都没意识到。

上回说到,ExecutorService接口新增了submit(),可以接收Runnable/Callable,并且支持返回值。而AbstractExecutorService则“初步”实现了submit():

但从源码来看,submit()仅仅做了Runnable/Callable的统一包装,具体的任务执行还是交给Executor#execute()。也就是通过模板方法模式,把具体的实现交给了子类。而这个“子类”,一般就是指ThreadPoolExecutor,本篇文章我们将研究它是如何执行上级指派的任务(Task)的。

开头先给大家留一个问题,请把下面的代码拷贝到本地执行:

@Slf4j
public class ThreadPoolDebug {
    public static void main(String[] args) {

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                1,                           // 核心线程数1
                2,                           // 最大线程数2
                1,                           // 存活时间设为1小时,避免测试期间线程被回收
                TimeUnit.HOURS,
                new ArrayBlockingQueue<>(1)  // 阻塞队列长度为1
        );

        threadPoolExecutor.execute(() -> {
            log.info("{}:执行第1个任务...", Thread.currentThread().getName());
            sleep(100);
        });

        sleep(1);

        threadPoolExecutor.execute(() -> {
            log.info("{}:执行第2个任务...", Thread.currentThread().getName());
            sleep(100);

        });

        sleep(1);

        threadPoolExecutor.execute(() -> {
            log.info("{}:执行第3个任务...", Thread.currentThread().getName());
            sleep(100);
        });

        sleep(1);

        threadPoolExecutor.execute(() -> {
            log.info("{}:执行第4个任务...", Thread.currentThread().getName());
            sleep(100);
        });

        sleep(1);

        log.info("main结束");
    }

    private static void sleep(int seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

运行结果为:

我的问题是:第2个任务去哪了呢?后面揭晓。

execute()执行流程

AbstractExecutorService#submit()底层调用了execute(),也就是说主要逻辑都在execute()中:

而ThreadPoolExecutor对execute()做了具体实现:

鄙人虽然日语专业出身,但语言都是相通的,姑且翻译一下:

public void execute(Runnable command) { // 这里的command,就是submit()里封装的FutureTask
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     * 分3步处理:
     * 
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     * 如果当前线程数少于corePoolSize,就新开一个【核心线程】处理当前请求
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     * 如果corePoolSize已经满了,就把当前任务【加入任务队列】等待执行
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     * 如果任务队列也满了,尝试开启新的【非核心线程】,如果开启失败(超过maximumPoolSize),则执行【拒绝策略】
     * 
     */
    
    //(1)获取ctl。ctl用来标记线程池状态(高3位),线程个数(低29位)
    int c = ctl.get();

    //(2)当前线程池线程个数是否小于corePoolSize,小于则【创建核心线程】处理当前任务
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }

    //(3)如果线程池处于RUNNING状态,则添加任务到阻塞队列(3.x这些分支可以不看,和主流程关系不大,都是兜底操作)
    if (isRunning(c) && workQueue.offer(command)) {

        //(3.1)二次检查
        int recheck = ctl.get();
        //(3.2)如果当前线程池状态不是RUNNING则从队列删除任务,并执行拒绝策略
        if (! isRunning(recheck) && remove(command))
            reject(command);

        //(3.3)否者如果当前线程池线程空,则添加一个线程(原则上可以设置所有线程空闲回收,所以可能为空)
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    //(4)第3步失败,说明队列满了,则【新增非核心线程】处理当前任务
    else if (!addWorker(command, false))
        // (4.1) 非核心线程已达上限,触发拒绝策略
        reject(command);
}

execute()本身思路很明确,它安排了任务处理的总流程:

  • 优先使用核心线程处理任务
  • 核心线程满了,优先入队
  • 等到队列满了,尝试开启非核心线程处理任务
  • 如果非核心线程也满了,那就没办法了,执行拒绝策略

看到这我们恍然大悟:原来那些年死记硬背的“线程池执行过程”出自execute()内部的注释!

回到开头那个问题:第2个任务去哪了?(注意,所有任务sleep 100秒,可以理解为每个线程无法处理其他任务)

  • 第1个任务:当前线程数为0 < corePoolSize,于是创建核心线程处理任务1 【参见(2)】
  • 第2个任务:当前线程数为1,不满足 currentSize < corePoolSize,于是任务2进入任务队列 【参见(3)】
  • 第3个任务:当前线程数为1,不满足 currentSize < corePoolSize,并且队列也满了,于是创建非核心线程处理当前任务 【参见(4)】
  • 第4个任务:当前线程数为2,不满足 currentSize < corePoolSize,并且队列也满了,总线程数也达到上限,触发拒绝策略 【参见(4.1)】

由于任务1和3分别霸占着核心线程和非核心线程,所以任务队列里的任务2没有线程去处理。但实际上,只要你等上100秒,会发现任务2就被处理了:

因为此时任务1和3已经被处理完了,线程又空闲下来了,于是回到任务队列里拿任务。

核心线程是如何处理任务的?

  • 优先使用核心线程处理任务
  • 核心线程满了,优先入队
  • 等到队列满了,尝试开启非核心线程处理任务
  • 如果非核心线程也满了,那就没办法了,执行拒绝策略

以上4种情况,除了拒绝策略,核心线程处理任务的流程相对简单,所以我们优先理解核心线程的任务处理流程(其他的都差不多)。

public void execute(Runnable command) { // 这里的command,就是submit()里封装的FutureTask
    if (command == null)
        throw new NullPointerException();
    
    //(1)获取ctl。ctl用来标记线程池状态(高3位),线程个数(低29位)
    int c = ctl.get();

    //(2)当前线程池线程个数是否小于corePoolSize,小于则【创建核心线程】处理当前任务。
    if (workerCountOf(c) < corePoolSize) {
        // addWorker()第二个参数代表是否核心线程
        if (addWorker(command, true))
            // 注意,核心线程的创建、执行都在addWorker()方法中,一旦任务提交给核心线程,整个execute()就结束了
            return;
        c = ctl.get();
    }
    
    // ...
}

addWorker()的大概流程是:

  • 先把task封装成Worker
  • 经过一系列步骤,启动Worker里的Thread,线程开启后会执行Worker里的Task

大致流程就是这样,接下来再探究细节。

我们一起来看看Worker是啥:

看上面Worker的构造器,我们不难发现 Worker = Thread + Task:

  • getThreadFactory().new Thread()会创建一个线程
  • Task则是Executor#execute(task)提交的那个任务

之前在多线程基础篇里我们提到过一个变种用法:

ThreadPoolExecutor中Worker也用了类似的写法:

右图Worker#run()又调用ThreadPoolExecutor#runWorker():

整个调用链是:

注意,Executor#execute()和FutureTask#run()之间不是同步调用,而是通过线程池的异步线程执行的。

也就是说,线程池里的Worker=Thread+Task,当线程启动后,会执行Worker#run(),但Worker并不是真正的任务,所以又要转一层,然后线程最终执行Worker内部的Task,也就是执行我们提交的任务。

线程池的生产消费模型

核心线程的处理逻辑虽然绕,但理清楚以后还是简单的。但从上面的介绍来看,你会发现和普通的new Thread(target).start()没太大区别,不是说线程池本质是生产消费模型吗?ThreadPoolExecutor的生产者是谁,消费者又是谁,什么时候消费、如何消费?

前面提到过,如果往线程池不断提交任务,大致会经历4个阶段:

  • 核心线程处理任务
  • 任务进入任务队列等待
  • 非核心线程处理任务
  • 拒绝策略

任务进入队列即为生产,而当核心线程/非核心线程处理完手头的任务并空闲时,就会从workQueue获取任务并处理,这就是ThreadPoolExecutor的生产消费模型。

ThreadPoolExecutor中,线程池是workers,任务队列时workQueue,不要搞混了

阻塞队列

ThreadPoolExecutor的生产消费模型是通过阻塞队列实现的,我们可以在创建ThreadPoolExecutor时指定使用哪种阻塞队列:

比如文章开头Demo中,我们使用了ArrayBlockingQueue。关于阻塞队列,之前篇章已经介绍过了,就不展开了。

生产者

我们往线程池提交任务的过程就是生产的过程:

public void execute(Runnable command) { // 这里的command,就是submit()里封装的FutureTask
    if (command == null)
        throw new NullPointerException();
    
    int c = ctl.get();

    // 假设核心线程数已满,跳过这一步
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }

    // workQueue.offer(command):尝试把任务加入到workQueue(任务队列),这个workQueue就是new ThreadPoolExecutor()时指定的BlockingQueue
    if (isRunning(c) && workQueue.offer(command)) {
        // ...
    }
    
    else if (!addWorker(command, false))
        reject(command);
}

有一点需要特别注意,之前我们模拟阻塞队列时,对于入队、出队两个操作都会进行阻塞(put、take),但ThreadPoolExecutor显然不允许这样做:

调用方提交一个任务给线程池,本来就是为了异步,结果你特么地把人家缠住了(阻塞)...

失败抛异常

失败返回特殊值

阻塞

阻塞(指定超时时间)

插入

add(e)

offer(e)

put(e)

offer(e, time, unit)

删除

remove()

poll()

take()

poll(time, unit)

查询

element()

peek()

所以ThreadPoolExecutor并没有对入队进行阻塞,而是判断队列长度是否已满,如果满了就返回false,execute()流程继续往下触发拒绝策略。

具体有哪些拒绝策略,大家自行了解。如果必要,我们也可以自定义拒绝策略,并在new ThreadPoolExecutor()时传入。

消费者

核心/非核心线程处理完手头任务后,是如何从任务队列获取新任务的呢?还记得ThreadPoolExecutor#runWorker()吗,异步线程开启后最终会调用它。

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    // ...
    try {
        // 线程开启后,进入循环:当前任务不为空 || 队列任务不为空
        while (task != null || (task = getTask()) != null) {
            // ...
            try {
                // ...
                task.run();
                // ...
            } finally {
                // ...
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

整个调用路径是:ThreadPoolExecutor#execute() ==> addWorker() ==> runWorker() ==> getTask()(绿色表示交给子线程执行)

也就是说,addWorker()可不是简单地创建线程并执行当前任务就完事了,runWoker()内部会循环,看看队列里有没有任务需要处理(getTask),是个很热心的小伙子!

线程的复用与销毁

大家有没有想过,一般来说new Thread().start()执行完目标任务后,就会自然销毁。那么线程池是如何做到任务跑完了之后线程不销毁的呢?更准确地说:

核心线程是如何复用的?

上面提到过,runWorker()内部其实是一个循环,不断地从任务队列中获取任务并执行。我们可以去看看getTask():

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    // 尝试循环获取任务
    for (;;) {
        // 看不懂,跳过
        int c = ctl.get();
        int rs = runStateOf(c);

        // 看不懂,跳过 Check if queue empty only if necessary.
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        // 看不懂,跳过
        int wc = workerCountOf(c);

        // 是否需要检测超时:允许所有线程超时回收(包括核心线程) || 当前线程数超过核心线程
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;


        // 超时了,跳出循环
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            // 是否需要检测超时
            // 1.需要:poll阻塞获取,等待keepAliveTime,等待结束就返回,不管有没有获取到任务
            // 2.不需要:一直阻塞,直到获取结果
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            
            // r==null,任务为空,timedOut=true
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

复用

如果把上面代码按某一个逻辑分支做简化:

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    // 尝试循环获取任务
    for (;;) {

        // 是否需要检测超时:当前线程数超过核心线程
        boolean timed = wc > corePoolSize;

        // 超时了,return null
        if (timed && timedOut) {
            return null;
        }

        try {
            // 是否需要检测超时
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
} 

也就是说,如果是核心线程,timed永远为false,那么就会调用workQueue.take()一直阻塞下去,直到有新的任务提交进来。但是处理结束后,还是会进入循环,周而复始。由于线程永远处于阻塞等待任务、执行任务、继续阻塞等待任务的死循环中,也就永远不会销毁了。

所以线程池之所以能复用线程,仅仅是 keeps threads busy working 罢了。

销毁

部分同学可能会有的疑惑:

  • 万一当前执行任务的是核心线程,却被销毁了咋办?
  • 怎样才算销毁了线程?

这里特别说明一下:

  • 所谓的“核心线程不会被销毁”,并不是指某些特定的线程不会被销毁,而是说无论线程怎么销毁,最终要保证池中活跃线程数不小于corePoolSize!
  • 所谓的线程销毁,其实就是让任务继续往下走,执行完了也就结束了,和new Thread().start()是一样的

如果不考虑allowCoreThreadTimeOut(一般不会刻意设置为true):

  • 那么当线程数不超过corePoolSize时,每一个线程都是核心线程,此时并不需要进行“超时检测”,所以线程会直接调用BlockingQueue#take()阻塞等待,直到有新的任务被提交。即使跳出getTask(),回到runWorker()执行完新的任务,也别指望线程就这么结束了,因为runWorker()本身也是循环,又会回到getTask()...从宏观上来看,就形成了所谓的“线程池Thread复用”
  • 当前线程数超过corePoolSize,那么就会getTask()里的循环就会进行“超时检测”。所谓的超时检测,其实就是“阻塞等待keepAliveTime”,等待结束直接返回,不论是否拿到任务。假设任务为null,就会跳过if判断,设置timeOut=true(线程当前数超过了corePoolSize && 这次又取不到任务,说明啥?线程池没任务,空闲了,所以施主你该走了)

设置完timeOut=true后还要重新回到循环:

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?

    // 尝试循环获取任务
    for (;;) {
        
        // 省略部分代码...

        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        // 此时条件成立
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            // 执行成功
            if (compareAndDecrementWorkerCount(c))
                // 返回空任务,跳出当前循环。而外层runWorker()由于 task==null,也会跳出循环
                return null;
            continue;
        }

        try {
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
} 

也就是说,只有当task=null && 队列为空 && 当前线程数超过corePoolSize,当前线程才可能连续跳过getTask()循环、runWorker()循环,最终执行完任务被销毁(太难了,一个线程想死却死不了)...

如何验证上面的结论呢?

@Slf4j
public class ThreadPoolDebug {
    public static void main(String[] args) {

        // 核心线程1,最大线程2,提交4个任务:第1个任务交给核心线程、第2个任务入队、第3个任务交给非核心线程、第4个任务被拒绝
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                1,
                2,
                1,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(1)
        );

        threadPoolExecutor.execute(() -> {
            log.info("{}:执行第1个任务...", Thread.currentThread().getName());
            sleep(3);
        });

        threadPoolExecutor.execute(() -> {
            log.info("{}:执行第2个任务...", Thread.currentThread().getName());
            sleep(3);

        });

        threadPoolExecutor.execute(() -> {
            log.info("{}:执行第3个任务...", Thread.currentThread().getName());
            sleep(3);

        });

        threadPoolExecutor.execute(() -> {
            log.info("{}:执行第4个任务...", Thread.currentThread().getName());
            sleep(3);

        });

        log.info("main结束");
    }

    private static void sleep(int seconds) {
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

执行完队列中的第2个任务后,return null,任务全部执行完毕,线程run()结束,线程销毁。

小结

如果把FutureTask也算上,实际上有4种执行多线程任务的方式:

  • Thread:
    • 重写Thread的run()
    • 通过Thread的构造器传入Runnable实例
    • 通过Thread的构造器传入Runnable实例(FutureTask,内部包装了Runnable/Callable)
  • 线程池:
    • 通过线程池(Runnable/Callable都行)

Thread和线程池看起来是单个线程和线程群体的关系,但实际上Thread和线程池还有个连接点:FutureTask。无论是Thread还是线程池,都可以接收FutureTask,只不过Thread使用FutureTask时需要我们在外部自己包装好Runnable和Callable,而线程池把这个操作内化了。

大家如果有兴趣,可以继续扩展上一篇的山寨ThreadPool,让构造器支持拒绝策略和ThreadFactory,这样7大参数都全了。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值