JDK ThreadPoolExecutor核心原理与实践

本文详细介绍了JDK中ThreadPoolExecutor的构造、执行流程、线程池状态管理、Worker内置类分析、拒绝策略以及二次开发实践。通过对核心参数的解释和常见线程池类型的探讨,揭示了线程池的工作原理。此外,还讲解了shutdown()执行流程,分析了Worker为何继承AQS以及四种拒绝策略的执行逻辑,最后提出了在业务场景中如何利用ThreadPoolExecutor进行优化。
摘要由CSDN通过智能技术生成

一、内容概括

本文内容主要围绕JDK中的ThreadPoolExecutor展开,首先描述了ThreadPoolExecutor的构造流程以及内部状态管理的机理,随后用大量篇幅深入源码探究了ThreadPoolExecutor线程分配、任务处理、拒绝策略、启动停止等过程,其中对Worker内置类进行重点分析,内容不仅包含其工作原理,更对其设计思路进行了一定分析。文章内容既包含了源码流程分析,还具有设计思路探讨和二次开发实践。

JDK ThreadPoolExecutor核心原理与实践

二、构造ThreadPoolExecutor

2.1 线程池参数列表

大家可以通过如下构造方法创建线程池(其实还有其它构造器,大家可以深入源码进行查看,但最终都是调用下面的构造器创建线程池);

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    ...
}

其中的构造参数的作用如下:

  • corePoolSize:核心线程数。提交任务时,当线程池中的线程数 小于 corePoolSize 时,会 新 创建一个核心线程执行任务。当线程数 等于 corePoolSize 时,会将任务 添加进任务队列。
  • maximumPoolSize:最大线程数。提交任务时,当 任务队列已满 并且线程池中的总线程数 不大于 maximumPoolSize 时,线程池会令非核心线程执行提交的任务。当 大于 maximumPoolSize 时,会执行拒绝策略。
  • keepAliveTime:非核心线程 空闲时 的存活时间。
  • unit:keepAliveTime 的单位。
  • workQueue:任务队列(阻塞队列)。
  • threadFactory:线程工厂。线程池用来新创建线程的工厂类。
  • handler:拒绝策略,线程池遇到无法处理的情况时会执行该拒绝策略选择抛弃或忽略任务等。

2.2 执行流程概述

由构造参数的作用我们可知,线程池中由几个重要的组件:核心线程池 、 空闲(非核心)线程池 和 阻塞队列。这里首先给出线程池的核心执行流程图,大家首先对其有个印象,之后分析源码就会轻松一些了。

下面对流程图中一些注释说明下:cap表示池的容量,size表示池中正在运行的线程数。对于阻塞队列来说,cap表示队列容量,size表示已经入队的任务数量。cpS<cpc表示运行中的核心线程数小于线程池设置核心线程数的情况。

JDK ThreadPoolExecutor核心原理与实践

1)当核心线程池 未 “满” 时,会创建新的核心线程执行提交的任务。这里的 “满” 指的是核心线程池中的数量(size)小于容量(cap),此时会通过线程工厂新创建线程执行提交任务。

2)当核心线程池 已 “满” 时,会将提交的任务push进任务队列中,等待核心线程的释放。一旦核心线程释放后,将会从任务队列中pull task继续执行。因为使用的是阻塞队列,对于已经释放的核心线程,也会阻塞在获取任务的过程中。

3)当任务队列也满了时(这里的满是指真的满了,当然暂不考虑无界队列情况),会从空闲线程池中继续创建线程执行提交的任务。但空闲线程池中的线程是有存活时间(keepAliveTime)的,当线程执行完任务后,只能存活 keepAliveTime 时长,时间一过,线程就得被销毁。

4)当空闲线程池的线程数不断增加,直到ThreadPoolExecutor中的总线程数大于 maximumPoolSize 时,会拒绝执行任务,将提交的任务交给 RejectedExecutionHandler 进行后续处理。

上面所说的核心线程池和空闲线程池只是抽象出来的一个概念,后面我们将对其具体内容进行分析。

2.3 常用线程池

在进入 ThreadPoolExecutor 的源码分析前,我们先介绍下常用的线程池(其实并不常用,只是JDK自带了)。这些线程池可由 Executors 这个工具类(或叫线程池工厂)来创建。

2.3.1 FixedThreadPool

固定线程数线程池的创建方式如下:其中核心线程数与最大线程数固定且相等,采用以链表为底层结构的无界阻塞队列。

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

特点

  • 核心线程数与最大线程数相等,因此不会创建空闲线程。keepAliveTime 设置与否无关紧要。
  • 采用无界队列,任务会被无限添加,直至内存溢出(OOM)。
  • 由于无界队列不可能被占满,任务在执行前不可能被拒绝(前提是线程池一直处于运行状态)。

应用场景

  • 适用于线程数固定的场景
  • 适用负载比较重的服务器

2.3.2 SingleThreadExecutor

单线程线程池的创建方式如下:其中核心线程数与最大线程数都为1,采用以链表为底层结构的无界阻塞队列。

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

特点

  • 与 FixedThreadPool 类似,只是线程数为1而已。

应用场景

  • 适用单线程的场景。
  • 适用于对提交任务的处理有顺序性要求的场景。

2.3.3 CachedThreadPool

缓冲线程池的创建方式如下:其中核心线程数为0,最大线程数为Integer.MAX_VALUE(可以理解为无穷大)。采用同步阻塞队列。

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

特点

  • 核心线程数为0,则初始就创建空闲线程,并且空闲线程的只能等待任务60s,60s内没有提交任务,空闲线程将被销毁。
  • 最大线程数为无穷大,这样会造成巨量线程同时运行,CPU负载过高,导致应用崩溃。
  • 采用同步阻塞队列,即队列不存储任务。提交一个消费一个。由于最大线程数为无穷大,因此,只要提交任务就一定会被消费(应用未崩溃前)。

应用场景

  • 适用于耗时短、异步的小程序。
  • 适用于负载较轻的服务器。

三、线程池状态以及活跃线程数

ThreadPoolExecutor 中有两个非常重要的参数:线程池状态 (rs) 以及 活跃线程数(wc)。前者用于标识当前线程池的状态,并根据状态量来控制线程池应该做什么;后者用于标识活跃线程数,根据数量控制应该在核心线程池还是空闲线程池创建线程。

ThreadPoolExecutor 用一个 Integer 变量(ctl)来设置这两个参数。我们知道,在不同操作系统下,Java 中的 Integer 变量都是32位,ThreadPoolExecutor 使用前3位(31~29)表示线程池状态,用后29位(28~0)表示活跃线程数。

JDK ThreadPoolExecutor核心原理与实践

这样设置的目的是什么呢?

我们知道,在并发场景中同时维护两个变量的代价是非常大的,往往需要进行加锁来保证两个变量的变化是原子性的。而将两个参数用一个变量维护,便只需一条语句就能保证两个变量的原子性。这种方式大大降低了使用过程中的并发问题。

有了上面的概念,我们从源码层面看看 ThreadPoolExecutor 的几种状态,以及 ThreadPoolExecutor 如何同时操作状态和活跃线程数这两个参数的。

ThreadPoolExecutor 关于状态初始化的源码如下:

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;

ThreadPoolExecutor 使用原子 Integer 定义了 ctl 变量。ctl 在一个int中包装了活跃线程数和线程池运行时状态两个变量。为了达到这样的目的,ThreadPoolExecutor 的线程数被限制在 2^29-1(大约500 million)个,而不是 2^31-1(2 billion)个,因为前3位被用于标识 ThreadPoolExecutor 的状态。如果未来 ThreadPoolExecutor 中的线程数不够用了,可以把 ctl 设置为原子 long 类型,再调整下相应的掩码就行了。

COUNT_BITS 概念上用于表示状态位与线程数位的分界值,实际用于状态变量等移位操作。此处为 Integer.sixze-3=32-3=29。

CAPACITY 表示 ThreadPoolExecutor 的最大容量。由下图可以看出,经过移位操作后,一个int值的后29位达到最大值:全为1。这29位表示活跃线程数,全为1时表明达到 ThreadPoolExecutor 能容纳的最大线程数。前3位为0&#x

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值