深入浅出理解ThreadPoolExecutor

文章目录

文章目录

  • 文章目录
  • 前言
  • 为什么要使用线程池?
  • 线程池使用方式
    • Executors创建线程池
      • newCachedThreadPool
      • newFixedThreadPool
      • newScheduledThreadPool
      • newSingleThreadExecutor
      • 四种线程池对比
    • ThreadPoolExecutor创建线程池
      • 线程池关闭
      • 线程池的状态监控
  • ThreadPoolExecutor源码分析
    • ThreadPoolExecutor工作流程
    • 内部结构
      • 核心参数——ctl
      • 核心组件——Worker
      • 核心实现——execute()
      • 线程销毁——processWorkerExit()
      • 小结
  • 线程池业务场景实战
    • 通过Spring托管线程池
    • 线程池在Dubbo中的应用
  • 结语

前言

在互联网的开发场景下,很多业务场景下我们需要使用到多线程的技术,从 Java 5 开始,JDK中 提供了线程池的实现,线程池就是一个线程的容器,每次只执行额定数量的线程。java.util.concurrent包中提供了线程池类ThreadPoolExecutor类来管理线程,同时提供了工厂类Executors简化线程池的使用。

本篇,我们将来详细的理解一下ThreadPoolExecutor的使用方式及其内部核心实现机制,让你明明白白的使用好线程池,Let’s do it!

为什么要使用线程池?

在执行一个异步任务或并发任务时,往往是通过直接new Thread()方法来创建新的线程,这样做弊端较多,Java的线程体系,是基于操作系统的线程来进行实现的,不做控制的大量的创建线程,会极大的消耗操作系统资源。

因此,更好的解决方案是合理地利用线程池,线程池的优势很明显,具体如下:

  • 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
  • 提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
  • 方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率;
  • 更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。

线程池使用方式

java.util.concurrent包中提供了多种线程池的创建方式,我们可以直接使用ThreadPoolExecutor类直接创建一个线程池,也可以使用工厂类Executors创建,下面我们分别了解一下这几种创建的方式。

Executors创建线程池

Executors类是java.util.concurrent提供的一个创建线程池的工厂类,使用该类可以方便的创建线程池,此类提供的几种方法,支持创建四种类型的线程池,分别是:newCachedThreadPool、newFixedThreadPool、newScheduledThreadPool、newSingleThreadExecutor。

newCachedThreadPool

创建一个可缓存的无界线程池,该方法无参数。当线程池中的线程空闲时间超过60s则会自动回收该线程,当任务超过线程池的线程数则创建新线程。

线程池的大小上限为Integer.MAX_VALUE,可看做是无限大。

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

使用示例:

   /**
     * 创建无边界大小的线程池
     */
    public static void createCachedThreadPool() {
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        final CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            final int currentIndex = i;
            cachedThreadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + ", currentIndex is : " + currentIndex);
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("全部线程执行完毕");
    }

newFixedThreadPool

创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。

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

使用示例:

   /**
     * 创建固定大小的线程池
     */
    public static void createFixedThreadPool() {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
        final CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            final int currentIndex = i;
            fixedThreadPool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + ", currentIndex is : " + currentIndex);
                countDownLatch.countDown();
            });
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("全部线程执行完毕");
    }

newScheduledThreadPool

创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
}


public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
}

使用示例:

这里创建了一个调度的线程池,执行两个任务,第一个任务延迟1秒后执行,第二个任务为周期性任务,延迟2秒后,每三秒执行一次

    /**
     * 创建给定延迟后运行命令或者定期地执行的线程池
     */
    public static void createScheduledThreadPool() {
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
        final CountDownLatch countDownLatch = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            final int currentIndex = i;
            //定时执行一次的任务,延迟1s后执行
            scheduledThreadPool.schedule(() -> {
                System.out.println(Thread.currentThread().getName() + ", currentIndex is : " + currentIndex);
                countDownLatch.countDown();
            }, 1, TimeUnit.SECONDS);
            //周期性地执行任务,延迟2s后,每3s一次地周期性执行任务
            scheduledThreadPool.scheduleAtFixedRate(() -> System.out.println(Thread.currentThread().getName() + ", every 3s execute done."), 2, 3, TimeUnit.SECONDS);
        }
    }

执行结果:

pool-1-thread-1, currentIndex is : 0
pool-1-thread-1, currentIndex is : 1
pool-1-thread-4, currentIndex is : 4
pool-1-thread-3, currentIndex is : 3
pool-1-thread-2, currentIndex is : 2

pool-1-thread-5, every 3s execute done.
pool-1-thread-3, every 3s execute done.
pool-1-thread-4, every 3s execute done.
pool-1-thread-2, every 3s execute done.
pool-1-thread-1, every 3s execute done.

pool-1-thread-5, every 3s execute done.
pool-1-thread-4, every 3s execute done.
pool-1-thread-5, every 3s execute done.
pool-1-thread-3, every 3s execute done.
pool-1-thread-2, every 3s execute done.

可以看到,第一个任务执行完毕后,开始执行定时调度型任务,按照指定的时间周期执行。

该线程池提供了多个方法:

方法 说明
schedule(Runnable command, long delay, TimeUnit unit) 延迟一定时间后执行Runnable任务
schedule(Callable callable, long delay, TimeUnit unit) 延迟一定时间后执行Callable任务
scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 延迟一定时间后,以间隔period时间的频率周期性地执行任务
scheduleWithFixedDelay(Runnable command, long initialDelay, long delay,TimeUnit unit) 与scheduleAtFixedRate()很类似,但是不同的是scheduleWithFixedDelay()的周期时间间隔是以上一个任务执行结束到下一个任务开始执行的间隔,而scheduleAtFixedRate()方法的周期时间间隔是以上一个任务开始执行到下一个任务开始执行的间隔,也就是这一些任务系列的触发时间都是可预知的

newSingleThreadExecutor

创建一个单线程的线程池,以无界队列方式来运行该线程。当多个任务提交到单线程线程池中,线程池将逐个去进行执行,未执行的任务将放入无界队列进行等待。

public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService(
            new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()));
}

使用示例:

/**
     * 创建单线程的线程池
     */
    public static void createSingleThreadPool() {
        ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
        singleThreadPool.execute(()-> System.out.println(Thread.currentThread().getName()));
    }

四种线程池对比

线程池方法 初始化线程池数 最大线程池数 线程池中线程存活时间 时间单位 工作队列
newCachedThreadPool 0 Integer.MAX_VALUE 60 SynchronousQueue
newFixedThreadPool 入参指定大小 入参指定大小 0 毫秒 LinkedBlockingQueue
newScheduledThreadPool 入参指定大小 Integer.MAX_VALUE 0 微秒 DelayedWorkQueue
newSingleThreadExecutor 1 1 0 毫秒 LinkedBlockingQueue

ThreadPoolExecutor创建线程池

以上,我们介绍了通过工具类Executors创建线程池的几种方式,Executors实际上是JDK封装好的线程池工具类,可以让开发者简单方便的创建线程池,而无需关心内在细节,事实上,Executors中的这些方法最终都是通过ThreadPoolExecutor类来完成的,当有一些场景需要更细粒度的控制的线程池,可以使用ThreadPoolExecutor方法创建线程池。

    /**
     * 使用ThreadPoolExecutor创建线程池
     */
    public void createThreadPoolExecutor() {

        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 10L, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(1000),
                new ThreadPoolExecutor.AbortPolicy());

        final CountDownLatch countDownLatch = new CountDownLatch(8);
        for (int i = 0; i < 8; i++) {
            final int currentIndex = i;
            System.out.println("提交第" + i + "个线程");
            threadPoolExecutor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + ", currentIndex is : " + currentIndex);
                countDownLatch.countDown();
            });
        }
        System.out.println("全部提交完毕");
        try {
            System.out.println("准备等待线程池任务执行完毕");
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("全部线程执行完毕");
    }

接下来看一下ThreadPoolExecutor的中的各个参数的含义。

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }    

ThreadPoolExecutor提供了四个构造方法,我们可以选择要传入的参数,如果不传入,则会默认指定,来分别看一下这些参数含义:

  • corePoolSize:核心线程数,即线程池中一直存活着的线程的最小数量。需要注意的是,核心线程数并不代表线程池启动后,就会立即创建这么多线程,只有当线程池接收到我们提交给他的任务后, 它才会去创建并启动一定数量的核心线程来执行这些任务。这种默认的核心线程的创建启动机制,有助于降低系统资源的消耗。

  • maximumPoolSize:线程池中允许创建的最大线程数量,当线程池的当前的核心线程全部在运行中,此时线程池接收到新的执行任务后,首先会判断线程池内部的阻塞队列 workQueue 中是否还有空间,如果有,则会将任务放入阻塞队列中,否则,会创建新的线程去进行执行;当线程数达到了最大线程数后,线程池接收到新的任务后,将会触发拒绝策略。

  • keepAliveTime:当线程池中的线程数大于核心线程数是,如果一个线程处于空闲状态,超过keepAliveTime 时间将终止该线程。这个参数的设定,需要考虑具体情况:如果要执行的任务相对较多,并且每个任务执行的时间比较短,那么可以为该参数设置一个相对较大的数值,以提高线程的利用率。

    如果执行的任务相对较少, 线程池使用率相对较低, 那么可以先将该参数设置为一个较小的数值, 通过超时停止的机制来降低系统线程资源的开销, 后续如果发现线程池的使用率逐渐增高以后, 线程池会根据当前提交的任务数自动创建新的线程。

  • unit:keepAliveTime的时间单位。

  • workQueue:线程池中存放执行任务的队列,用于保存等待执行的任务。当提交一个新的任务到线程池以后, 线程池会根据当前池子中正在运行着的线程的数量, 指定出对该任务相应的处理方式,主要有以下几种处理方式:

    1、如果线程池中正在运行的线程数少于核心线程数,那么线程池总是倾向于创建一个新线程来执行该任务,而不是将该任务提交到该队列 workQueue 中进行等待。

    2、如果线程池中正在运行的线程数不少于核心线程数,那么线程池总是倾向于将该任务先提交到队列 workQueue 中先让其等待,而不是创建一个新线程来执行该任务。

    3、如果线程池中正在运行的线程数不少于核心线程数,并且线程池中的阻塞队列也满了使得该任务入队失败,那么线程池会去判断当前池子中运行的线程数是否已经等于了该线程池允许运行的最大线程数 maximumPoolSize。如果发现已经等于了,说明池子已满,无法再继续创建新的线程了,那么就会拒绝执行该任务。如果发现运行的线程数小于池子允许的最大线程数,那么就会创建一个线程(这里创建的线程是非核心线程)来执行该任务。

    排队策略:

    1. 直接提交。工作队列的默认选项是SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
    2. 无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
    3. 有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
    • threadFactory:线程工厂,用于创建线程。如果我们在创建线程池的时候未指定该threadFactory参数,线程池则会使用 Executors.defaultThreadFactory()方法创建默认的线程工厂,如果我们想要为线程工厂创建的线程设置一些特殊的属性,例如:设置见名知意的名字,设置特定的优先级等等,那么我们就需要自己去实现 ThreadFactory 接口,并在实现其抽象方法 newThread()的时候,使用Thread类包含 threadName(线程名字)的那个构造方法就可以指定线程的名字(通常可以指定见名知意的名字),还可以用 setPriority() 方法为线程设置特定的优先级等。然后在创建线程池的时候,将我们自己实现的 ThreadFactory 接口的实现类对象作为 threadFactory 参数的值传递给线程池的构造方法即可。
  • handler:拒绝策略,当向线程池提交新的任务,线程池无法执行的时候,将会触发拒绝策略。当出现以下情况时,拒绝策略将会触发:

    • 当线程池处于SHUTDOWN状态时
    • 当线程池的线程数已达到最大线程数且全部为运行状态,同时阻塞队列的容量已满

    ThreadPoolExecutor支持四种拒绝策略,分别是:

    1、AbortPolicy: 直接拒绝策略,抛出 RejectedExecutionException 异常,该策略也是线程池的默认拒绝策略。

    2、CallerRunsPolicy:将被拒绝的任务放在ThreadPoolExecutor.execute()方法所在的那个线程中执行。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。

    3、DiscardPolicy:将被拒绝的任务直接删除。

    4、DiscardOldestPolicy:当线程池没有关闭的情况下,会将阻塞队列队首的那个任务从队列中移除,然后将被拒绝的任务加入队列的队尾。

线程池关闭

ThreadPoolExector提供了shutdown()或shutdownNow()方法来关闭线程池。

  • shutdown:按过去执行已提交任务的顺序发起一个有序的关闭,但是不接受新任务。如果已经关闭,则调用没有其他作用。
  • shutdownNow:尝试停止所有的活动执行任务、暂停等待任务的处理,并返回等待执行的任务列表。在从此方法返回的任务队列中排空(移除)这些任务。

中断采用interrupt方法,所以无法响应中断的任务可能永远无法终止。但调用上述的两个关闭之一,isShutdown()方法返回值为true,当所有任务都已关闭,表示线程池关闭完成,则isTerminated()方法返回值为true。当需要立刻中断所有的线程,不一定需要执行完任务,可直接调用shutdownNow()方法。

线程池的状态监控

ThreadPoolExector提供的多个方法,可以用于监控线程池的运行情况,参数如下:

  • getTaskCount:返回曾计划执行的近似任务总数。因为在计算期间任务和线程的状态可能动态改变,所以返回值只是一个近似值。
  • getCompletedTaskCount:返回已完成执行的近似任务总数。因为在计算期间任务和线程的状态可能动态改变,所以返回值只是一个近似值,但是该值在整个连续调用过程中不会减少。
  • getLargestPoolSize:线程池曾经创建过的最大线程数量,通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。
  • getPoolSize:线程池的线程数量。
  • getActiveCount:返回主动执行任务的近似线程数。

通过扩展线程池进行监控:继承线程池并重写线程池的beforeExecute(),afterExecute()和terminated()方法,可以在任务执行前、后和线程池关闭前自定义行为。如监控任务的平均执行时间,最大执行时间和最小执行时间等。

使用ThreadPoolExecutor直接创建线程池时,可以使用第三方的ThreadFactory,或者自己实现ThreadFactory接口,拓展更多的属性,例如设置线程名称、执行开始时间、优先级等等。

ThreadPoolExecutor源码分析

在上述的篇幅中,我们详尽的介绍了ThreadPoolExector线程池的使用方式,但了解线程池的使用方式,也需要了解线程池的实现与工作原理,才可以知其然也知其所以然,接下来,我们来对线程池的源码进行分析。

ThreadPoolExecutor工作流程

分析源码之前,下面我们用一张流程图对ThreadPoolExector的工作流程进行概览:

深入浅出理解ThreadPoolExecutor_第1张图片

内部结构

核心参数——ctl

在分析线程池工作原理的源码之前,我们先来看一下线程池内部的一些核心参数:

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

private static final int COUNT_BITS = Integer.SIZE - 3;

private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

private static int runStateOf(int c) { 
    return c & ~CAPACITY; 
}
private static int workerCountOf(int c) { 
    return c & CAPACITY; 
}
private static int ctlOf(int rs, int wc) {
    return rs | wc; 
}

ctl参数是线程池内部非常核心的一个参数,使用AtomicInteger表示,保证多线程操作下的线程安全性;

它控制着线程池的状态,一个ctl变量包含了两部分信息:线程池的运行状态和线程池内有效线程的数量。ctl是一个int类型,它用其高3位表示线程池运行状态,用低29位表示线程池内有效线程的数量。

关于这种控制方式,如果您看过ReentrantReadWriteLock的源码,一定不陌生这种方式,ReentrantReadWriteLock使用一个数值分别记录读写锁的获取次数,这种使用位操作控制的方式在concurrent包中非常常见,非常精妙。

线程池的运行状态分为以下几种状态:

  • RUNNING:运行状态,能接受新提交的任务, 并且也能处理阻塞队列中的任务
  • SHUTDOWN:关闭状态,不再接受新提交的任务, 但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时, 调用 shutdown()方法会使线程池进入到该状态。当然, finalize() 方法在执行过程中或许也会隐式地进入该状态
  • STOP:停止状态,不能接受新提交的任务,也不能处理阻塞队列中已保存的任务, 并且会中断正在处理中的任务。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态
  • TIDYING:清理状态,所有的任务都已终止了, workerCount (有效线程数) 为0, 线程池进入该状态后会调用 terminated() 方法以让该线程池进入TERMINATED 状态. 当线程池处于 SHUTDOWN 状态时, 如果此后线程池内没有线程了并且阻塞队列内也没有待执行的任务了, 线程池就会进入到该状态。当线程池处于 STOP 状态时,如果此后线程池内没有线程了, 线程池就会进入到该状态
  • TERMINATED:终止状态,当调用terminated() 方法后进入该状态

通过调用runStateOf() 方法,可以获取到当前线程池的运行状态

调用workerCountOf() 方法,可以获取到当前线程池的工作线程数目

对于我们来讲,我们可能更关注的状态是线程池是否在运行、是否准备停止、是否已经停止,
这里我们可以粗暴的预先给出结果:

runStateOf() < 0 时,表示线程池正在运行中

runStateOf() = 0 时,表示线程池准备停止

runStateOf() > 0 时,表示线程池进入停止状态(也可能是TIDYING或者TERMINATED)

核心组件——Worker

Worker是内部比较重要的一个对象,它是线程池内部的实际执行器,我们提交的Runnable方法,是由它进行执行的。Worker对象实现了AQS队列,也就是说它具备锁的能力,它自身实现了锁的一部分功能。

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
    
    private static final long serialVersionUID = 6138294804551838833L;

    final Thread thread;

    Runnable firstTask;

    volatile long completedTasks;

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

    public void run() {
        runWorker(this);
    }

    protected boolean isHeldExclusively() {
        return getState() != 0;
    }

    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    protected boolean tryRelease(int unused) {
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    public void lock()        { acquire(1); }
    public boolean tryLock()  { return tryAcquire(1); }
    public void unlock()      { release(1); }
    public boolean isLocked() { return isHeldExclusively(); }

    void interruptIfStarted() {
        Thread t;
        if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
            try {
                t.interrupt();
            } catch (SecurityException ignore) {
            }
        }
    }
}

核心实现——execute()

介绍完构造方法与内部结构的核心参数与对象,我们接下来看一下线程池的核心实现。

execute()方法是线程池的核心方法,提交一个Runnable任务给线程池,线程池进行执行,接下来我们来看分析一下实现机制:

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);
}

上面就是execute() 方法的实现,其接收一个Runnable的参数,我们对大体流程进行梳理,在对具体细节进行分析:

1、获取ctl值

2、判断当前工作线程池数是否小于核心线程数,如果是,新增工作线程执行任务,并结束流程

3、如果不是,说明当前工作线程数已经达到核心线程数值,则判断线程池是否是运行状态,如果是,尝试将任务加入到阻塞队列中(而不是新增线程);

如果成功,再次判断线程池是否是运行状态,如果已经不再是运行状态,将刚刚新增到阻塞队列中的任务移除,并执行拒绝策略;如果依旧是运行状态,判断当前工作线程数是否为0,如果是,增加工作线程,但不启动;

4、上述条件均不满足,则尝试新增工作线程执行任务,如果失败,执行拒绝策略

我们来分步骤进行分析:

首先来看一下新增工作线程的实现,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.
        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 {
        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()) // 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();
            }
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

流程稍长,我们一步一步来看:

1、进入无限循环,首先获取运行状态,判断运行状态是否正常

2、循环检查当前工作线程数是否大于最大容量,或者是否大于核心线程数或最大线程数(参数控制),如果校验通过,通过CAS操作增加工作线程数,并退出循环

3、前置校验全部通过后,进入新增工作线程部分,创建Worker对象,将提交的任务放入Worker中

4、获取锁,将Worker加入工作线程组中,释放锁,启动Worker线程

addWorker() 方法的大体流程如上,这里并没有进行特别详细的分析,由于整体流程并不复杂,较为核心的部分,是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 ((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 {
        //获取不到任务时,主动回收自己
        processWorkerExit(w, completedAbruptly);
    }
}

由于Worker本身是一个线程,当启动Worker时,会执行其run() 方法,在run() 中,调用了runWorker(),实现如上。

我们对其流程进行详细分析:

1、获取当前线程,以及Worker中的Runnable任务

2、获取锁,如果任务不为空,或getTask() 获取任务不为空时,进行while循环

3、首先会判断线程池的状态,以及任务线程的状态,是否处于被打断的状态

4、执行扩展方法beforeExecute(),在执行任务前需要进行的操作,ThreadPoolExecutor只定义了方法,没有进行实现,子类可以自行进行扩展

5、执行任务的run方法,真正的任务内容开始执行

6、执行扩展方法afterExecute(),同beforeExecute()

7、释放锁,将任务的引用置空,并进行任务执行结束后的状态处理

深入浅出理解ThreadPoolExecutor_第2张图片

这是一个较为就简单的示意图,当我们提交一个任务到ThreadPoolExecutor线程池时,如果当前线程池没有工作线程,则会新建一个,去执行提交的任务,当工作线程数达到核心线程数时,再提交的任务会加入到阻塞队列中去;

当核心工作线程执行完毕它之前的任务时,则会从阻塞队列中获取任务,再次进行执行,而获取的操作,则是在getTask()方法中实现:

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?
    for (;;) {
        int c = ctl.get();
        int rs = runStateOf(c);
        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;
        }
    }
}

getTask() 主要流程我们看一下:

1、获取线程池运行状态

2、检查线程池的状态,如果已经是STOP及以上的状态,或者已经SHUTDOWN,队列也是空的时候,直接return null,并将Worker数量-1

3、获取从阻塞队列获取任务是否超时标志,allowCoreThreadTimeOut参数,字面意思是否允许核心线程超时,即如果我们设置为false,那么只有当线程数wc大于corePoolSize的时候才会超时,更直接的意思就是,如果设置allowCoreThreadTimeOut为false,那么线程池在达到corePoolSize个工作线程之前,不会让闲置的工作线程退出,如果设置为true,那么核心工作线程空闲后,会立刻退出

4、从队列中取任务,根据timed选择是有时间期限的等待还是无时间期限的等待,这里的等待时间是我们在构造方法中传入的keepAliveTime

看到这里,我们就可以理解在文章开头中提到的,为什么线程池可以重用已存在的线程: 因为当工作线程达到核心线程数目后,如果当前线程池中没有需要执行的任务,线程池将进入空闲状态,核心工作线程将进入等待;此时当有新任务提交时,将会进入到阻塞队列中,核心线程会从阻塞队列中获取任务,进行执行,反复这个过程。

线程销毁——processWorkerExit()

经过上述对线程池大体工作流程的介绍,我们已经了解了线程池大体的工作机制,接下来我们进入最后一个部分,线程池无可执行任务后,线程池中线程的销毁流程。

线程池中线程的销毁依赖JVM自动的回收,线程池做的工作是根据当前线程池的状态维护一定数量的线程引用,防止这部分线程被JVM回收,当线程池决定哪些线程需要回收时,只需要将其引用消除即可。

Worker被创建出来后,就会不断地进行轮询,然后获取任务去执行,核心线程可以无限等待获取任务,非核心线程要限时获取任务。当Worker无法获取到任务,也就是获取的任务为空时,循环会结束,Worker会主动消除自身在线程池内的引用。

下面我们来看一下线程池销毁空闲线程的具体实现:

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // 1、判断流程异常标志位,如果出现用户的执行任务中抛出异常打断流程,更新workerCount - 1
    if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
        decrementWorkerCount();

    final ReentrantLock mainLock = this.mainLock;
    // 2、获取锁资源
    mainLock.lock();
    try {
        // 3、更新completedTaskCount,并将Worker移除工作队列
        completedTaskCount += w.completedTasks;
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }
	// 4、尝试终止线程池,检查线程池是否需要进入终止流程
    tryTerminate();
	
    // 5、是否需要增加worker线程
    int c = ctl.get();
    // 如果状态是running、shutdown,即tryTerminate()没有成功终止线程池,尝试再添加一个worker
    if (runStateLessThan(c, STOP)) {
        // 判断流程异常标志位为false的情况,即没有task任务可以获取而完成的,计算min,并根据当前worker数量判断是否需要addWorker()
        if (!completedAbruptly) {
            // allowCoreThreadTimeOut默认为false,即min默认为corePoolSize
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            // 如果min为0,即不需要维持核心线程数量,且workQueue不为空,至少保持一个线程
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            // 如果线程数量大于最少数量,直接返回,否则下面至少要addWorker一个
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        // 添加一个没有firstTask的worker
        // 只要Worker是completedAbruptly流程异常终止的,或者线程数量小于要维护的数量,就新添一个Worker线程,即使是shutdown状态
        addWorker(null, false);
    }
}

processWorkerExit()方法在runWorker()方法的finally代码块中,当工作队列中没有待执行的任务时,线程池将会执行回收Worker线程的操作,主要流程如下:

1、判断流程异常标志位,如果出现用户的执行任务中抛出异常打断流程,更新workerCount - 1

2、获取锁资源

3、更新completedTaskCount,并将Worker移除工作队列

4、尝试终止线程池,检查线程池是否需要进入终止流程

5、检查是否需要增加worker线程,这里分为两部分逻辑:

  • 第一部分,如果Worker是completedAbruptly流程异常终止的,直接新增一个Worker;
  • 第二部分,如果Worker是正常流程销毁回收的,则检查当前Worker的数量是否小于核心线程数,如果小于,新增Worker,否则退出流程

小结

综上,我们对线程池的整体工作流程的核心源码实现进行了了解,简单的说,ThreadPoolExecutor实现了一套生产消费者模型,类比于一条工厂流水线,流水线上的工人们(Worker)等待着流水线(BlockingQueue)上流转的零件(Runnable)来进行组装工作,流水线上零件较多,大于额定的负载量时,就会增派人手一起过来帮忙,流水线上零件较少时,则会让一部分工人们下去休息,希望用这样的例子来帮助读者更好的理解线程池的工作流程。

线程池业务场景实战

通过Spring托管线程池

在实际业务开发场景中,我们可能会遇见很多方法需要使用异步的处理方式,但在业务代码中多处创建线程池有时会有些繁琐,针对这种场景,我们可以将线程池交予Spring进行托管,仅需在需要使用异步的方法时,使用@Async注解,即可执行异步操作。

SpringBoot开启@Async:

@Configuration
@EnableAsync
public class AsyncConfig {
   ...
}

使用@Async:

@Async  //标注使用  
public void asyncMethodWithVoidReturnType() {
    System.out.println("Execute method asynchronously. "+ Thread.currentThread().getName());  
}

默认情况下,Spring会使用SimpleAsyncTaskExecutor开启异步,但并不推荐使用这种方式,因为SimpleAsyncTaskExecutor并非使用线程池处理异步,而是每次创建一个线程来进行处理。

#SimpleAsyncTaskExecutor#doExecute()

	/**
	 * Template method for the actual execution of a task.
	 * 

The default implementation creates a new Thread and starts it. * @param task the Runnable to execute * @see #setThreadFactory * @see #createThread * @see java.lang.Thread#start() */ protected void doExecute(Runnable task) { Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task)); thread.start(); }

推荐方式:

我们可以自行指定异步线程池:

@Configuration
@EnableAsync
public class AsyncConfig {
    
    @Bean(name = "threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor() {
        return new ThreadPoolTaskExecutor();
    }
}

@Async("threadPoolTaskExecutor")
public void asyncMethodWithConfiguredExecutor() {
    System.out.println("Execute method with configured executor - " + Thread.currentThread().getName());
}

也可以通过实现AsyncConfigurer接口的方式自定义线程池:

@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {
    
    @Override
    public Executor getAsyncExecutor() {
        return new ThreadPoolTaskExecutor();
    }
}

线程池在Dubbo中的应用

在使用Dubbo提供RPC接口时,如果某些Dubbo Service API非常重要,我们希望将其与其他接口隔离开,来保障性能,可以为单独的Dubbo接口指定线程池。

Dubbo Service支持指定provider,在provider中配置指定自定义线程池,即可完成RPC接口的单独线程池隔离。

示例:

// 指定provider配置中的threadpool属性,我们自定义的线程池
<dubbo:provider id="p1" threadpool="myThreadPool" default=false/>

实现自定义线程池:

src
 |-main
    |-java
        |-com
            |-xxx
                |-XxxThreadPool.java (实现ThreadPool接口)
    |-resources
        |-META-INF
            |-dubbo
                |-org.apache.dubbo.common.threadpool.ThreadPool (纯文本文件,内容为:myThreadPool=com.xxx.XxxThreadPool)


import org.apache.dubbo.common.threadpool.ThreadPool;
import java.util.concurrent.Executor;
 
public class XxxThreadPool implements ThreadPool {
    public Executor getExecutor() {
        // ...
    }
}

Dubbo SPI配置自定义线程池 META-INF/dubbo/org.apache.dubbo.common.threadpool.ThreadPool:

myThreadPool=com.xxx.XxxThreadPool

在DubboService接口中指定provider:

@Service
@DubboService(version = "1.0.0", timeout = 1000, provider = "p1")
@Slf4j
public class DubboApiImpl implements DubboApi {
    
    @Override
    public String hello()  {
        return "hello world";
    }
}

结语

本篇,我们介绍了ThreadPoolExecutor的使用方式及其工作原理,线程池在我们的日常业务场景中的应用非常的广泛,希望可以对您有所帮助。

你可能感兴趣的:(聊聊Java并发,java,开发语言,线程池,Threadpool)