『深入学习Java』(八) ThreadPoolExecutor 线程池

·  阅读 67

前言

老生常谈的线程池,我们来重新梳理一遍。

Executor 简介

Java 中的线程池都是以 Executor 为根基。Executor是执行提交的Runnable任务的对象。该接口提供了一种将任务提交与运行解耦的机制,其中包括隐藏了线程使用、调度等的细节。

在HotSpot VM的线程模型中,Java线程(java.lang.Thread)被一对一映射为本地操作系统线程。Java线程启动时会创建一个本地操作系统线程;当该Java线程终止时,这个操作系统线程也会被回收。操作系统会调度所有线程并将它们分配给可用的CPU。

Doug Lea 大佬在源码注释中告诉我们,通常要使用Executor而不是显式创建线程。

典型用法如下:

Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
复制代码

我们日常最常用的是 ThreadPoolExecutor

image

ThreadPoolExecutor 简介

一个ExecutorService ,它使用可能的多个池线程之一执行每个提交的任务,通常使用Executors工厂方法进行配置。

线程池解决了两个不同的问题:

  • 减少了每个任务的调用开销,它们通常在执行大量异步任务时提供改进的性能
  • 提供了一种限制和管理资源的方法,包括执行一组线程任务时消耗的线程。

此外,每个 ThreadPoolExecutor 还维护了一些基本的统计信息,例如完成任务的数量。

为了在广泛的上下文中有用,这个类提供了许多可调整的参数和 hooks 。

此外,还通过 Executors 工厂类,提供一些常见的使用场景预配置。

  • ExecutorsnewCachedThreadPool - 无界线程池,具有自动线程回收功能
  • ExecutorsnewFixedThreadPool - 固定大小的线程池
  • ExecutorsnewSingleThreadExecutor - 单线程线程池
  • newScheduledThreadPool - 定时及周期性任务

此处 《Java 开发手册(黄山版)》一、编程规约 (七) 并发处理

4.【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方

式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

1)FixedThreadPool 和 SingleThreadPool:

允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

2)CachedThreadPool:

允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

3)ScheduledThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。

虽说《Java 开发手册(黄山版)》中规定了不允许使用 Executors 创建线程池,但是还是看实际情况吧,确定业务达不到那么多请求,使用起来也无妨。

毕竟我在看 Nacos 源码,发现里面也有使用Executors的情况存在。

ThreadPoolExecutor 核心参数

画了一张比较简略的图,表示大致的意思。

image

这是一个参数比较全的 ThreadPoolExecutor 构造器,通过这个构造器,我们可以对线程池的配置一探究竟。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 
{...}
复制代码
  • 核心线程数与最大线程数 ThreadPoolExecutor 将根据 corePoolSize 和 maximumPoolSize 自动调整线程池大小。 当在方法execute(Runnable)中提交了一个新任务,并且运行的线程少于 corePoolSize 时,即使其他工作线程处于空闲状态,也会创建一个新线程来处理该请求。 如果运行的线程数多于 corePoolSize 但少于 maximumPoolSize,则仅当队列已满时才会创建新线程。

    • 通过将 corePoolSize 和 maximumPoolSize 设置为相同,可以创建一个固定大小的线程池。
    • 通过将 maximumPoolSize 设置为基本上无界的值,例如Integer.MAX_VALUE ,可以允许池容纳任意数量的并发任务。 核心和最大池大小可以在构造时设置,也可以使用setCorePoolSize()setMaximumPoolSize()动态更改。
  • 保活时间 即非核心线程,无任务可处理时,最长存活时间。
  • 线程工厂 ThreadPoolExecutor 使用 ThreadFactory 创建新线程。 如果没有另外指定,则使用 ExecutorsdefaultThreadFactory,它创建的线程都处于相同的 ThreadGroup 并具有相同的优先级和非守护进程状态。 通过提供不同的 ThreadFactory,可以更改线程的名称、线程组、优先级、守护进程状态等。
  • 任务队列

    任何BlockingQueue都可以用来传输和保存提交的任务。

使用队列与线程池大小相关:

  • 如果运行的线程少于 corePoolSize,则 Executor 添加新线程而不加入队列。
  • 如果达到 corePoolSize 或有更多线程正在运行,Executor 排队请求而不添加新线程。
  • 如果请求无法排队,则会创建一个新线程。如果超过 maximumPoolSize,在这种情况下,该任务将被拒绝。

排队策略有以下三种:

  • 直接交接。 默认工作队列是 SynchronousQueue ,它把任务直接交给线程处理。如果没有立即可用的线程来运行任务,将构造一个新线程。
  • 无界队列。 所有 corePoolSize 线程都在处理任务时,新任务将在无界队列中等待。 当线程池使用无界队列时,不会创建超过 corePoolSize 个线程,即最大线程数参数无效。
  • 有界队列。 有界队列(例如ArrayBlockingQueue )在与 maximumPoolSizes 一起使用时,可以控制服务器压力。

队列大小和线程池大小可以相互配合:使用大队列小池可以减少 CPU 使用率、上下文切换开销,但会导致吞吐量降低。使用小队列大池, CPU 的利用率更高,但可能会有频繁线程上下文切换,这也会降低吞吐量。 所以,在使用时,根据实际情况来配置队列大小和线程池大小是非常有必要的。

  • 拒绝策略

    当 Executor 关闭时,或者 有界队列饱和时,在方法execute(Runnable)中提交的新任务将被拒绝。无论哪种情况, execute方法都会调用RejectedExecutionHandler.rejectedExecution(Runnable, ThreadPoolExecutor)方法。

    提供了四种处理策略:

    • 默认ThreadPoolExecutor.AbortPolicy - 抛出RejectedExecutionException 异常。
    • ThreadPoolExecutor.CallerRunsPolicy - 调用 execute 线程(Main)运行任务。这是一种简单的反馈控制机制,可以减慢提交新任务的速度。
    • ThreadPoolExecutor.DiscardPolicy - 丢弃任务。
    • ThreadPoolExecutor.DiscardOldestPolicy - 如果 executor 没有关闭,则丢弃工作队列头部的任务,然后重试执行(可能再次失败,导致重复此操作。)

此外,可以自定义 RejectedExecutionHandler 子类。

ThreadPoolExecutor 的其他特性

  • 核心线程预热 默认情况下,即使是核心线程也仅在新任务到达时才最初创建和启动,但可以使用方法prestartCoreThreadp启动一个core thread 或者 prestartAllCoreThreads启动所有核心线程。

  • Hooks 方法

    beforeExecute(Thread, Runnable)afterExecute(Runnable, Throwable)方法,这些方法在执行每个任务之前和之后调用。

    可用于操纵执行环境;例如,重新初始化 ThreadLocals、收集统计信息或添加日志条目。此外,可以重写方法terminated以执行任何特殊处理,Executor 完全终止时调用。

    如果钩子或回调方法抛出异常,内部工作线程可能会依次失败并终止。

  • 获取队列 方法getQueue()允许访问工作队列进行监视和调试。强烈建议不要将此方法用于任何其他目的。

ThreadPoolExecutor 的执行过程

execute(Runnable command)

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
     
        int c = ctl.get();
        // 当线程数量少于 corePoolSize时,每次都新创建线程执行任务。
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 持有线程数量大于corePoolSize,任务入队。
        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);
    }
复制代码

Worker 与 addWorker

通过 execute 方法,我们可以看到:ThreadPoolExcutor 会将 Runable 封装成为一个 Worker,然后添加 Worker 到 ThreadPoolExcuter 中。Worker 本身还继承了 AQS 、实现了 Runnable。

 private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
        // worker 持有的线程。
        final Thread thread;
        // worker 持有的第一个任务
        Runnable firstTask;
        // worker 完成的任务数量
        volatile long completedTasks;
        // ......
        // 这里需要注意一下,创建 Work 的时候,会创建一个新线程。并且把自己本身作为任务,传递给线程。
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
​
        public void run() {
            runWorker(this);
        }
}
复制代码

这里先提前科普一个小知识

break retry 跳到retry处,且不再进入循环 continue retry 跳到retry处,且再次进入循环

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        // 前置状态判断
        if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
            return false;
        // 这一部分,主要是判断线程池数量是否满足。
        for (;;) {
            int wc = workerCountOf(c);
            // 如果线程总数超过了容量,或者超过了(corePoolSize,maxPoolSize),直接返回false。
            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 {
        // 创建工作线程
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            // 获取线程池实例的全局锁。防止并发添加线程,导致线程数量超过预期值。
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // 再次检查线程池状态
                int rs = runStateOf(ctl.get());
                if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) throw new IllegalThreadStateException();
                    // 添加到工作线程中。
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize) largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                // 释放线程池全局锁
                mainLock.unlock();
            }
            if (workerAdded) {
                // 启动线程。这里实际上也就是调用 Worker#run()
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        // ... 处理添加启动线程失败的逻辑...
    }
    return workerStarted;
}
复制代码

runWorker

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        // 这里开始处理 task 了。
        // 如果指定有 firshTask,先处理 firshTask。
        // 否则,从 阻塞队列中获取 Task 执行,一直到队列中没有任务。
        while (task != null || (task = getTask()) != null) {
            w.lock();
            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 
                } finally {
                    // 处理任务之后做些什么。
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                // 任务数量 ++
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        // 如果报错了,或者因为没有 Task 了。
        // 这个线程就要结束了,处理身后事。
        processWorkerExit(w, completedAbruptly);
    }
}
复制代码

getTask()

private Runnable getTask() {
    // 从 workQueue 中获取任务是否超时。
    boolean timedOut = false;
    // 开始自旋
    for (;;) {
        // 状态检查
        int c = ctl.get();
        int rs = runStateOf(c);
        // 如果线程池即将关闭,队列中没任务了。
        // 直接返回null。
        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 {
            // 如果非核心线程不需要常驻,通过带有超时时间的获取方法,获取任务。
            // 否则调用阻塞获取。
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}
复制代码

简略地说就是,线程被创建出来后,处理完指定给自己的第一个任务后,就去阻塞队列中轮询获取下一个任务进行处理。如果一直有任务的话,也就一直处理任务,不会销毁线程了。如果阻塞队中没有任务了,就根据设置决定是否从 Set中,移除线程。

小结

这一篇中,我们主要重新复习 Java 中的线程池,包括了:

  • 基本概念。
  • 核心配置项。
  • 其他特性例 - 如核心线程预热、钩子方法之类。
  • 核心的运行方法。

但,还有一些知识面没有覆盖到,例如 BlockingQueue 阻塞队列等等。

我们后面继续~

参考资料

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改