Java线程池原理解析

0 阅读15分钟

线程池是什么

线程是 Java 程序执行的最小单位。线程池就是管理一系列线程的资源池,提供了管理线程并且基于资源池内的线程执行多任务的功能。

为什么用线程池

在多任务的场景下,为了提升执行速度,通常会采取多线程并行处理的方式。这种情况下,线程的数量可能会膨胀到很夸张的地步,又或者说出现线程频繁创建销毁的情况。

而系统所能提供的资源是有限的,特别是在移动端。线程的创建销毁和调度都会占用系统资源。使用线程池可以维护多个线程执行任务,并且对线程进行复用,从而降低资源消耗,提高响应速度。

常用的线程池类型

JDK 中提供了一些常用的线程池类型,我们可以根据不同的应用场景调用 Executors 的静态方法构建它们。以下所列的线程池类型都是通过 ThreadPoolExecutor 构建的,只是传入的参数不同。当然也有其他用不同类构建的线程池,这里暂时不展开介绍,先了解经典的 ThreadPoolExecutor

CachedThreadPool

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(
        0, 
        Integer.MAX_VALUE,
        60L,
        TimeUnit.SECONDS,
        new SynchronousQueue<Runnable>()
    );
}

没有核心线程,但可以拥有近似不限数量的非核心线程。任务到来时,如果没有可用线程,会创建新的线程执行;如果有可用线程,则重用该线程执行。线程在空闲一段时间后,会自动终止。

FixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(
        nThreads,
        nThreads,
        0L, 
        TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<Runnable>());
}

只拥有固定数量的核心线程。当所有核心线程都在工作的时候,新的任务会放到一个默认容量为 Integer.MAX_VALUE 的队列中等待执行。

SingleThreadExecutor

SingleThreadExecutorFixedThreadPool 类似,只拥有一个核心线程。

ThreadPoolExecutor

以上可见,常用的线程池都是通过 ThreadPoolExecutor 构造的,当然开发者也可以按需构造 ThreadPoolExecutor

其实线程池的实现类不止 ThreadPoolExecutor ,但由于其使用比较广泛,是应该优先学习的。接下来将会介绍 ThreadPoolExecutor ,让读者对其有一个概念。

ThreadPoolExecutor 的构造

以下是 ThreadPoolExecutor 比较有代表性的构造函数及其参数:

public ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler
) {
    ...
}
参数含义
corePoolSize核心线程数量。默认配置下,核心线程空闲时不会因为超时而退出,核心线程的存在能提高线程池的响应速度。
maximumPoolSize最大线程数量,包括核心线程数的总线程数。核心线程数以外、最大线程数以内的线程并不是常驻的,这部分线程在空闲时有一个最大存活时间。
keepAliveTime核心线程数以外的空闲线程最大存活时间。默认配置下只作用于非核心线程;如果开启 allowCoreThreadTimeOut,核心线程也会受影响。
unitkeepAliveTime 的时间单位。
workQueue任务队列。当前线程数达到核心线程数后,任务会尝试加入这个队列。
threadFactory构建线程的工厂类对象。
handler线程池拒绝执行任务时触发的回调。

线程池的状态

可以理解成线程池的生命周期,不同的状态线程池会有不同的表现,读者可以先有一个概念:

状态接受新任务处理队列任务说明
RUNNING正常运行状态。
SHUTDOWN调用 shutdown 后进入该状态,不接受新任务,但会继续处理队列中剩余的任务和正在执行的任务。
STOP调用 shutdownNow 后进入该状态,不继续处理队列任务,并且尝试中断正在执行的任务。
TIDYING线程池达到终止条件后进入该状态,会触发 terminated 方法,让用户做一些收尾工作。
TERMINATEDterminated 方法执行完成后进入该状态。

Worker

Worker 是线程池的一个内部类。线程在线程池中的形式就是 Worker。它有以下特点:

每个 Worker 都会持有一个线程对象

Worker 在构造时,会通过 ThreadFactory 创建线程对象并持有。

Worker(Runnable firstTask) {
   setState(-1); // inhibit interrupts until runWorker
   this.firstTask = firstTask;
   this.thread = getThreadFactory().newThread(this);
}

Worker 线程的运行

通过 ThreadFactory 创建线程对象时,会将 this 传入到 ThreadFactorynewThread 中,以下是 ThreadFactory 的默认实现类 DefaultThreadFactorynewThread 方法。

public Thread newThread(Runnable r) {
    Thread t = new Thread(group, r,
            namePrefix + threadNumber.getAndIncrement(),
            0);
    if (t.isDaemon())
        t.setDaemon(false);
    if (t.getPriority() != Thread.NORM_PRIORITY)
        t.setPriority(Thread.NORM_PRIORITY);
    return t;
}

可以看到传入的参数是一个 Runnable 对象,并且传入了 Thread 的构造,而 Worker 本身就是 Runnable ,所以可以推断,当 Worker 中的线程被开启时,会执行 Workerrun 方法。

再来看一下 Workerrun 方法:

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable {
    public void run() {
        runWorker(this);
    }
}

可以看到 run 方法执行了 runWorker 方法。

本章小结: 线程池开启线程最终执行的是线程池的 runWorker 方法。

Worker 与任务执行

execute:任务提交流程

execute 是线程池执行任务的入口,需要传入一个任务:

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    // 1
    int c = ctl.get();
    // 2
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 3
    if (isRunning(c) && workQueue.offer(command)) {
        // 3.1
        int recheck = ctl.get();
        // 3.2
        if (! isRunning(recheck) && remove(command))
            reject(command);
        // 3.3
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 4
    else if (!addWorker(command, false))
        reject(command);
}

流程说明:

1:获取一个 intc。这个 c 包含了两种数据:第一种是 Worker 的数量,第二种是线程池的状态。在需要时,对 ctl 进行位运算就可以取出相应的数据。

2:获取 Worker 的数量。如果 Worker 数量小于核心线程数,则通过 addWorker 添加 Worker 去执行 command 所代表的任务。

addWorker 的第二个参数是限定 Worker 的数量边界,后面讲解 addWorker 的时候会介绍,可以理解为是否开启一个核心 WorkeraddWorker 会返回代表添加成功的布尔值:如果返回 true,表示添加成功,流程结束;如果返回 false,表示添加失败,会重新获取 c

3:从 c 获取状态。如果状态是运行中,则尝试将任务加入到任务队列。如果加入成功,则继续执行以下逻辑。

3.1:重新获取 ctl,因为此时线程池的状态可能发生了变化。

3.2:如果此时不在运行中,则从任务队列中移除这个任务。如果移除成功,则执行拒绝策略回调。

3.3:来到这里有两种情况:一是状态仍然为运行中,二是从任务队列移除失败。这两种情况都会判断 Worker 数是否为 0。如果为 0,则创建一个不携带任务的 Worker,让它从队列中获取任务执行。为什么是不携带任务,因为这个任务已经被加入到任务队列了却没有 Worker 处理,不管是刚刚列出的情况一还是情况二都需要一个 Worker 去处理这个任务,而 Worker 本身不需要携带任务,只需要从队列取出任务执行就好了。

4:来到这里有两种情况:一是状态不是运行中,二是加入任务队列失败。如果是第一种情况,addWorker 会返回 false,因为线程池不在运行中,不会接受新的任务。第二种情况一般是队列已经满了,所以会尝试开启一个核心线程数以外的 Worker 并携带这个任务去执行。

本章小结: execute 的流程可以概括为先补核心线程,再尝试入队,入队失败后再尝试增加线程执行任务。

addWorker:增加 Worker

execute 的主要任务就是执行 addWorkeraddWorker 顾名思义,就是新增一个 Worker。它有两个参数:

参数含义
firstTask新创建的 Worker 首先要执行的任务。它可以是 execute 传入的新任务,也可以为空。
core是否以核心线程数作为边界条件。如果是 false,则以最大线程数作为边界条件。
private boolean addWorker(Runnable firstTask, boolean core) {
    // 1
    retry:
    for (int c = ctl.get();;) {
        // 1.1
        // Check if queue empty only if necessary.
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP)
                || firstTask != null
                || workQueue.isEmpty()))
            return false;
        // 1.2
        for (;;) {
            // 1.2.1
            if (workerCountOf(c)
                >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                return false;
            // 1.2.2
            if (compareAndIncrementWorkerCount(c))
                break retry;
            // 1.2.3
            c = ctl.get();  // Re-read ctl
            if (runStateAtLeast(c, SHUTDOWN))
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }
    // 2
    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        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 c = ctl.get();
                // 2.1
                if (isRunning(c) ||
                    (runStateLessThan(c, STOP) && firstTask == null)) {
                    if (t.getState() != Thread.State.NEW)
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    workerAdded = true;
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

流程说明:

1:首先进入一个双重 for 循环,主要作用是把控线程池状态与 Worker 数量的正确性,好执行下一步操作。线程池是支持异步的,ctl 随时都有可能变化,所以代码中有很多类似的判断逻辑。

1.1:想要通过这个 if return,首先线程池状态是 RUNNING 肯定能过。另外一种能过的情况是:线程池状态为 SHUTDOWN,并且 firstTask 为空,并且任务队列不为空。

为什么这种情况可以通过?因为 SHUTDOWN 状态可以继续执行任务队列里的任务,但是不能接受新的任务。所以当新创建的 Worker 没有携带任务,并且任务队列也有任务待执行时,这个 Worker 是被允许创建的。

1.2:内层 for 循环是为了确保可以顺利使 ctl 中的 Worker 数量增加 1。

1.2.1:第一道关卡,确保增加 Worker 后数量在边界条件以内。如果超出边界条件,返回 false,创建失败。

1.2.2:第二道关卡,compareAndIncrementWorkerCount 会对 ctl 中的 Worker 数量加 1,也就是会对 ctl 的值进行修改。如果修改成功,则跳出外层循环,走接下来的逻辑。

如果此时有其他线程也同时修改了 ctl,则 ctl 最新的值就不是 c 了,compareAndIncrementWorkerCount 会返回 false 并走到 1.2.3。

1.2.3:来到这里有个前提条件,那就是 ctl 发生了变化,但是并不确定是线程池状态发生了变化还是 Worker 数量发生了变化。如果有可能是前者,则 continue retry 重新进入外层循环重新检验线程池状态。为什么是有可能,因为 runStateAtLeast(c, SHUTDOWN) 返回 true 不一定就是状态变了,它有两种情况,第一种是从 RUNNING 切换到了 SHUTDOWN,第二种是一直在 SHUTDOWN,第一种情况是一定要重走外层循环的,那第二种情况既然线程池状态没变,那变得肯定是 Worker 数量,也就是说有可能其他线程给线程池增加了 Worker 并且处理了任务队列的任务,此时当前的 Worker 就没必要创建了,为了预防这种情况,也需要重新进入外层循环走一遍准入的逻辑。如果单纯只是 Worker 数量发生了变化,线程池状态一直是 RUNNING,那只需要重走内层的循环重新尝试增加 Worker 数量就行。

2:这里开始创建 Worker。先新建 Worker 对象并取出内部的 Thread,然后使用全局锁 mainLock 进行同步,再次获取最新的 ctl 并在 2.1 中校验状态。

状态没问题后,把新建的 Worker 加入集合,释放锁,之后调用 Worker 内部 Threadstart 方法开启线程。如果以上过程成功开启线程则返回 true,否则返回 false

本章小结: addWorker 的流程可以概括为:在状态允许且在数量边界内创建 Worker 并且返回创建结果。

runWorker:Worker 的运行

Worker 中的线程运行后,最终会执行线程池的 runWorker 方法,参数是线程对应的 Worker

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
            // 1
            if (
                (runStateAtLeast(ctl.get(), STOP) || (Thread.interrupted() && runStateAtLeast(ctl.get(), STOP)))
                && !wt.isInterrupted()
            ) {
                wt.interrupt();
            }
            try {
                beforeExecute(wt, task);
                try {
                    task.run();
                    afterExecute(task, null);
                } catch (Throwable ex) {
                    afterExecute(task, ex);
                    throw ex;
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

runWorker 的逻辑比较直接:开启一个循环,先执行自身携带的任务,再通过 getTask 不断获取任务执行。要注意的是,如果通过 getTask 获取到的任务为空,则会终止循环,也同样意味着这个 Worker 已经结束运行。

执行过程中会以自身为锁进行同步。这个锁主要用于标记 Worker 是否是空闲中,以便在需要的时候中断线程。

重点看 1 处的条件判断。想要进入这个 if,其实会有两条分支。

分支 1:

runStateAtLeast(ctl.get(), STOP) && !wt.isInterrupted()

线程池状态至少为 STOP,并且线程还没被中断,则中断线程。这个很好理解,至少为 STOP 表示不接受新的任务与任务队列的任务,并中断正在执行的任务。

分支 2:

线程池状态小于 STOP
&& Thread.interrupted()
&& runStateAtLeast(ctl.get(), STOP)
&& !wt.isInterrupted()

这里通过 && 可以分割出 A、B、C、D 四个条件。

条件 A 不多做解释。

条件 B 是判断线程是否被中断过,这是有可能出现的,因为线程池并不完全掌控所有工作线程,这些线程是有可能被外部中断的。

条件 C 再次判断线程池状态是否为 STOP 以上。如果为 false,则分支 2 结束,因为状态小于 STOP,不应该中断线程。注意 Thread.interrupted() 会清除线程的中断状态,所以如果线程池状态没有到 STOP,相当于取消了线程的意外中断。

既然 C 为 true,则已经满足了中断线程的前置条件,条件 D 只是在 wt.interrupt() 之前再做一层保险,前面说了线程池并不完全掌控所有工作线程,如果此时线程已经被中断了,就没有必要再在这里中断一次了,重复中断甚至有可能抛异常。

分支 2 可以总结为:如果线程池的状态需要线程中断,则让线程进入或保持中断的状态,如果否,则将线程恢复到非中断的状态。

本章小结: runWorker 负责持续取任务、执行任务,并且在每次执行任务前处理线程的中断状态。

getTask:获取执行任务

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

    for (;;) {
        int c = ctl.get();

        // 1
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP) || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        // 2
        int wc = workerCountOf(c);

        // 3
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

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

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

流程说明:

1:检查状态。这里有两种情况会返回 null 去结束 Worker

  • 线程池状态为 STOP
  • 线程池状态为 SHUTDOWN,并且任务队列为空。

2:获取当前的 Worker 数。

3:timed 的意思是当前 Worker 是否会等待任务超时。这里有两种情况会让 timedtrue

  • allowCoreThreadTimeOuttrue,表示核心 Worker 会等待任务超时,同时也意味着线程池的所有 Worker 都是会超时的。
  • 核心线程数以内的 Worker 不会等待任务超时,但是总的 Worker 数已经超过了核心线程数,意味着当前调用 getTaskWorker 会等待任务超时。

4:这里是决定当前 Worker 是否可关闭的核心判断。以下是判断条件:

条件含义
`wc > maximumPoolSize(timed && timedOut)`Worker 数量大于设定的最大 Worker 数量,或者当前 Worker 已经超时。
`wc > 1workQueue.isEmpty()`当前 Worker 数量大于 1,或者任务队列已经空了。

条件 A 满足后,当前 Worker 已经是可以考虑关闭了,但同时也要满足条件 B,因为如果当前 Worker 关闭了,任务队列又有任务,会出现任务无 Worker 处理的情况。

如果 A、B 都满足,则可以关闭当前 Worker。 此时会使用 CAS 将 ctl 中的 Worker 数量减一:

  • 如果 CAS 成功,返回一个 null 给调用处,结束 Worker 的运行。
  • 如果 CAS 失败,有可能线程池状态或者 Worker 数量出现了变化,需要重新进入循环走状态判断。

5:从任务队列取任务。如果队列中有任务,会立刻返回;如果没有,则根据 timed 决定等待方式。

  • poll:等待任务一段时间,如果超过时间,则返回 null
  • take:一直等待任务,直到队列中存在任务。

接下来如果得到的任务为空,表示已超时,将 timedOut 赋值为 true,并进入下一轮循环。注意 timedOuttrue 不一定就马上结束当前 Worker,还要重新走以上逻辑,直到满足结束条件才会真正结束 Worker

本章小结: getTask 决定了 Worker 是继续等待任务,还是因为线程池状态、Worker 数量、等待超时等原因退出。

总结

ThreadPoolExecutor 的核心流程可以总结为:

  1. 提交任务时,优先补足核心 Worker
  2. 核心 Worker 已满后,优先把任务放入任务队列。
  3. 如果任务队列无法加入,再尝试创建核心线程数以外的 Worker
  4. 如果创建失败,则执行拒绝策略。
  5. Worker 运行后,会不断通过 getTask 从队列获取任务,直到线程池状态或超时条件要求它退出。

原文章地址:david-su.github.io/posts/%E7%B…