【高频面试点】-【线程池工作流程】-【其二】核心线程的刁钻角度

108 阅读12分钟

书接上回:【高频面试点】-【线程池工作流程】-【其一】工作线程数workerCount、线程池运行状态runState的设计及维护

1 前言

在线程池运行状态下,不断往里添加任务时,其内部工作机理相信各位小伙伴都已耳熟能详,即便没有研究过线程池内部实现原理,没有看过源码的同学,也应该像我前文提到的“那个朋友”一样,背八股文怎么也得背熟了。

可是,当你面试时,面试官满足的远远不止于能把主流程背出来,他往往会挑一些刁钻的角度,让毫无准备的你当场懵逼。

比如:

  1. 当你设置了核心线程数corePoolSize后,创建线程池时会立刻将这些线程预热创建出来吗?
  2. 我们都知道核心线程是不会被回收的,那线程池是如何实现让核心线程在没有任务时等待不回收呢?
  3. 线程池是如何“标记”核心线程的呢?如何在没有活儿干的时候让核心线程“待岗”,而非核心线程“直接走人”呢

    像极了自有员工和外包员工

2 核心线程会在线程池创建之初就创建吗?

如果你够聪明,即使你不知道答案,光看这个问题,你也能立刻回答:否。

因为这很明显,看似一个疑问句,实则充满了反问句的意味。答案自然是“否”,在解释为什么要这么设计之前,先来看看线程池的源码,理解一下“真相”。

2.1 核心线程随任务添加时创建源码分析

先看看任务添加的入口方法:execute

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    /*
     * Proceed in 3 steps:
     *
     * 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.
     *
     * 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.
     *
     * 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.
     */
    int c = ctl.get();
    // 线程池刚创建,workerCount自然是0,如果设置了corePoolSize > 0,该条件成立
    if (workerCountOf(c) < corePoolSize) {
        // addWorker方法就是添加任务,第二个参数为true,表示用核心线程执行任务
        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方法

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    // 存在并发场景,通过自旋来解决并发安全及重试问题
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);

        // Check if queue empty only if necessary.
        // 这里是在判断线程池runState状态以及内存队列,如果状态异常,会直接返回false
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;
        // 这里则是校验任务数量,判断正在执行的任务是否超过了设置的线程数
        for (;;) {
            int wc = workerCountOf(c);
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        // 走到这里,说明可以进行任务添加,则包装一个Worker对象
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int rs = runStateOf(ctl.get());
                // 再次校验线程池状态,相当于一个double check lock
                if (rs < SHUTDOWN ||
                    (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) // precheck that t is startable
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            // 终于一切顺利,完成任务添加后,直接开启worker线程,开始执行任务
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

关注上述源码中我添加的中文注释,代表了任务添加的几个关键步骤。结合最开始的execute方法即可得出结论:线程池刚开始的确没有预热初始化核心线程,是在addWorker方法添加任务的时候,通过Worker包装,并调用Thread#start()方法完成线程创建的

2.2 线程池为什么不在刚开始预热创建所有线程呢?

要回答这个问题,我可以很负责任的说,没有什么特别的原因,仅仅只是源码这样实现的而已,不要试图去猜测Doug Lea老爷子为什么这样写,有什么特殊的考虑,就像你不要去猜测“XX作者为啥要写窗外的倾盆大雨一样”,既不是为了衬托作者悲伤的心情,也不是为了表达哀怨的气氛,就只是此时正在下倾盆大雨一样。

但如果硬要说,依我的理解,我个人认为这种“懒加载”的方式可能更好,更能够节省CPU和内存资源。有的同学可能会说了,一开始预热好不是更好吗?创建线程开销这么大,高并发时造成任务延迟怎么办?

这种问题完全不必担心,因为如果你非要预热,那你就自行预热好了,你只需要在创建线程池后,调用execute方法添加一个空任务即可,就像这样:

ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());;

public void preHot() {
    executor.execute(() ->{ });
}

对没错,这样就行了。

然后你试着想想,如果Doug Lea将预热的逻辑写死了,而有的同学又不想要预热,或者说,有的同学代码写的稍微差点,又或者说,生产环境上核心线程数配置设置的很大很大。就没有一点儿回旋的余地,万一不小心将线程数设置的很大,岂不是开局即崩溃,资源直接干满。而这样写呢,相当于把“预热”的权利给了在座的各位自行选择

3 线程池如何区分“正式员工”和“外包员工”,并让“正式员工”不下岗,“外包员工”用完即辞退呢?

同学们都知道,线程池除了设置“核心线程数”,还能设置“最大线程数”,当后者数量大于前者,并且运行时核心线程数已满、阻塞队列已满,但此时仍有任务添加进来时,会开启“临时线程”进行处理。

这听起来,就像是我们熟悉的正式员工和外包员工的区别。

3.1 寻找源码中是如何进行区分的

还是要回到梦开始的地方,也就是execute函数,提交任务的入口方法

参考本文【2.1 核心线程随任务添加时创建源码分析】

addWorker函数即为提交任务的入口,但它有2个入参,源码注释是这样写的:

/* @param firstTask the task the new thread should run first (or
 * null if none). Workers are created with an initial first task
 * (in method execute()) to bypass queuing when there are fewer
 * than corePoolSize threads (in which case we always start one),
 * or when the queue is full (in which case we must bypass queue).
 * Initially idle threads are usually created via
 * prestartCoreThread or to replace other dying workers.
 *
 * @param core if true use corePoolSize as bound, else
 * maximumPoolSize. (A boolean indicator is used here rather than a
 * value to ensure reads of fresh values after checking other pool
 * state).
 * @return true if successful
 */
private boolean addWorker(Runnable firstTask, boolean core)

core这个boolean类型的参数就决定了是否使用核心线程去执行任务

但,真相真的是这样吗?

3.2 线程池中的“人人机会均等”

再回头看一眼【2.1 核心线程随任务添加时创建源码分析】中的addWorker源码,你会发现,针对core这个入参,全函数就一个地方进行了使用,就是在判断workerCount的时候:

for (;;) {
    int wc = workerCountOf(c);
    if (wc >= CAPACITY ||
        // 这里在使用,wc是workerCount(当前任务数),如果core为true则跟核心线程数比较,否则与最大线程数比较
        wc >= (core ? corePoolSize : maximumPoolSize))
        return false;
    if (compareAndIncrementWorkerCount(c))
        break retry;
    c = ctl.get();  // Re-read ctl
    if (runStateOf(c) != rs)
        continue retry;
    // else CAS failed due to workerCount change; retry inner loop
}

除此之外,再无其它使用之处,就连下方真正去构建Worker对象,开启线程,也没有将core参数传递进去,更没有标记哪个线程是核心线程。

啊这?!有些不合理呀,既然定义了核心线程,不标记的话我怎么知道应该释放哪一个,应该保留哪一个呢

这时候不妨坐下来仔细想想,我真的需要去标记核心和非核心线程吗?线程资源与人力资源不同,人力资源都是独立的个体,每个人都是不一样的,学历、经历、智商...,所以才有正式员工与外包员工之分。但线程,作为操作系统的一种资源,它没有区别,至少在线程池看来,视所有线程都是平等的个体,因此,秉承着“谁能干谁干,谁干不了退出”的原则。

在线程池内部,只对线程数量进行管理,每次有线程干完活儿后,看看当前还有多少线程在干活儿,跟我预先设定的名额差多少,多了,您就走,少了或刚刚好。就留下等活儿,就这么简单

image.png

上面的图例简单描述了这个逻辑关系和设计理念,假设了一个场景,设置的核心线程数=2,最大线程数=3,并且当前有3个线程在干活。当然,这是一个比较粗略的示意图,只是为了描述清楚线程池内部“人人平等”的设计理念。

3.3 核心线程不回收的原理探秘

既然没有对核心和非核心线程进行标记,那线程池内部又是如何对核心线程进行阻塞不释放,而对非核心线程在没有任务的时候释放的呢?废话不多说,直接上源码:

首先是runWorker函数,也就是Worker包装类对象实现的Runnable的run方法所调用的函数:

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        // 获取任务执行的逻辑,当执行完毕后,会调用getTask方法从队列中继续获取任务执行
        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 {
        // 非核心线程超时从阻塞队列中没有获取到任务后,会跳出while循环,走到这里,workerCount - 1
        processWorkerExit(w, completedAbruptly);
    }
}

关注源码中中文注释部分,该方法是执行业务逻辑的真正入口,当执行完毕后,会通过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);

        // Are workers subject to culling?
        // 关注这里,一般来说不手动设置,allowCoreThreadTimeOut参数为false,即不允许核心线程超时,因此会走到后面的逻辑判断,判断此时工作线程数是否大于核心线程数
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            /**
             * 如果此时工作线程数是否大于核心线程数,则该线程被视为非核心线程(外包员工),
             * 继续压榨一段时间(超时获取一下任务,基于poll方法等一下活儿,还有活儿就获取到任务后
             * 返回接着干,否则外层函数会跳出while循环,接着方法栈弹出,线程被释放),
             * 否则该线程被视为核心线程(正式员工),不解雇,基于take方法阻塞一直等到活儿来
             */
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

还是关注源码中我中文注释的部分。

所以看到了吗,控制核心线程一直不释放,非核心线程继续压榨一段时间后再释放的核心逻辑,就在于七大线程池参数之一的阻塞队列,利用阻塞队列的poll和take函数完美实现了该特性,妙哉!

4 所以再回头看看最初的问题,线程池有标记“核心”和“非核心”吗

自然是没有的,在线程池里真正做到了“人人平等”,并且实现了资本主义的精髓,来人即用,用完即走。如果平时活儿不多,现有正式员工慢慢干,活儿一多了就赶紧找外包干,干完走之前还要让你等一会儿,如果此时有活儿来了,不好意思先别着急走,干完了再说。

欲哭无泪,果然程序都来自于生活!

5 写在最后

线程池作为大家最常用的JDK工具之一,不但要知其然还要知其所以然,才能更好的帮助我们在工作中辅助业务实现,避免踩坑,提前预判,合理设置参数。

研究线程池也能帮助我们理解生活的不易,从而热爱生活,理解人生的真谛:没有谁一定是谁的谁,只有你自己,活在当下,享受当下,线程的执行就像一趟火车,终点并不重要,因为谁都能run完任务,只有沿途的风景才是最值得体会的,不是吗?打工人们