JUC并发编程基础(9)--线程池

线程池

线程池概述

线程池是一种基于池化思想管理线程的工具,经常出现在多线程服务器上,比如MySQL

池化,顾名思义,是为了最大化收益并最小化风险,而将资源统一在一起管理的一种思想。

由于存在许多的短时间任务,所以线程不断创建和销毁会有额外的代价,另外线程过多就会导致数量膨胀导致过分调度问题,而线程池就能够保证对内核的充分利用,还有一些其它的好处,例如:

  • **降低资源消耗:**通过池化技术重复利用已经创建线程,降低线程创建和销毁的代价。
  • **提高响应速度:**任务到达的时候,跳过了创建线程的步骤,可以立即执行。
  • **提高了线程的可管理性:**线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  • 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

并发环境下,系统不知道有多少任务需要执行(在任意时刻下),也不知道要给多少资源,就有了很多问题:

  • 一直需要申请、销毁资源和调度资源,会带来可能非常巨大的消耗。
  • 系统偏偏对这种一直申请资源的情况没有办法,可能会造成资源耗尽。
  • 资源申请的多了,你这个线程来一点,那个来一点,就无法管理内部的资源分布,稳定性大大降低。

这时候就需要线程池了,在Java中的体现是ThreadPoolExecutor类

线程池架构-总体设计

ThreadPoolExecutor类的继承关系为:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uuiDpEzO-1657634311522)(C:\Users\aMyth\AppData\Roaming\Typora\typora-user-images\image-20220712205556165.png)]

**顶层接口-Executor:**将任务提交和任务执行解耦,用户无需关注如何创建线程,如何调度,只需要提交任务的运行逻辑到Executor中即可。

**接口-ExecutorService:**增加了一些功能,概括为就是,为异步任务生成Future方法,并且提供了管控线程池的方法,比如停止啥的。

**抽象类-AbstractExecutorService:**上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。

**实现类-ThreadPoolExecutor:**实现最复杂的运行部分,ThreadPoolExecutor将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

ThreadPoolExecutor运行流程:

任务提交,判断是否立即执行或者拒绝任务,再或者让任务去缓冲队列呆着,就这三种状态。

执行任务的话就申请线程,当任务执行完之后,该线程就会继续获取新任务,当获取不到新任务时,线程回收。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m8ch6trH-1657634311525)(C:\Users\aMyth\AppData\Roaming\Typora\typora-user-images\image-20220712210710153.png)]

线程池-生命周期管理

线程池的运行状态和池内线程数量,没有存在两个变量中,而是用一个32位Integer存储的,通过高3位保存运行状态,低29位保存线程数量,可以减少有改动的时候,需要对俩变量进行一直匹配,防止占用锁资源。线程池提供了很多方法去获取当前运行状态、个数,都是基于位运算的方法。

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

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

//ctl就是保存运行状态和数量的变量,通过ctlof获取,存入该AtomicInteger类型变量

img

以上就是线程池的五种运行状态,生命周期转换如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C7URcyMH-1657634311544)(C:\Users\aMyth\AppData\Roaming\Typora\typora-user-images\image-20220712212058896.png)]

线程池-任务执行机制
任务调度
  1. 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
  2. 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
  3. 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
  4. 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
  5. 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。

图4 任务调度流程

任务缓冲

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d45gzVgL-1657634311556)(C:\Users\aMyth\AppData\Roaming\Typora\typora-user-images\image-20220712212840484.png)]

具体有哪些阻塞队列,可见上述《阻塞队列》部分。

任务申请

任务的执行有两种可能:一种是任务直接由新创建的线程执行。另一种是线程从任务队列中获取任务然后执行,第一种情况仅出现在线程初始创建的时候,第二种是线程获取任务绝大多数的情况。

线程需要从任务缓存模块中不断地取任务执行,帮助线程从阻塞队列中获取任务,实现线程管理模块和任务管理模块之间的通信。这部分策略由getTask方法实现,其执行流程如下图所示:

图6 获取任务流程图

任务拒绝

当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。

可以通过实现RejectedExecutionHandler接口去自定义拒绝策略,也可以是用JDK提供的拒绝策略。

img

线程池-Worker线程管理
Worker线程

为了掌握线程的状态并维护线程的生命周期,设计了线程池内的工作线程Worker。

private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
    final Thread thread;//Worker持有的线程
    Runnable firstTask;//初始化的任务,可以为null
}

Worker这个工作线程,实现了Runnable接口,并持有一个线程thread,一个初始化的任务firstTask。thread是在调用构造方法时通过ThreadFactory来创建的线程,可以用来执行任务;firstTask用它来保存传入的第一个任务,这个任务可以有也可以为null。如果这个值是非空的,那么线程就会在启动初期立即执行这个任务,也就对应核心线程创建时的情况;如果这个值是null,那么就需要创建一个线程去执行任务列表(workQueue)中的任务,也就是非核心线程的创建。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OncAJXWd-1657634311570)(C:\Users\aMyth\AppData\Roaming\Typora\typora-user-images\image-20220712214507539.png)]

Worker线程增加

增加线程是通过线程池中的addWorker方法,该方法的功能就是增加一个线程,该方法不考虑线程池是在哪个阶段增加的该线程,这个分配线程的策略是在上个步骤完成的,该步骤仅仅完成增加线程,并使它运行,最后返回是否成功这个结果。addWorker方法有两个参数:firstTask、core。firstTask参数用于指定新增的线程执行的第一个任务,该参数可以为空;core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize,其执行流程如下图所示:

图9 申请线程执行流程图

worker线程回收

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

try {
  while (task != null || (task = getTask()) != null) {
    //执行任务
  }
} finally {
  processWorkerExit(w, completedAbruptly);//获取不到任务时,主动回收自己
}
worker执行任务

在Worker类中的run方法调用了runWorker方法来执行任务,runWorker方法的执行过程如下:

1.while循环不断地通过getTask()方法获取任务。 2.getTask()方法从阻塞队列中取任务。 3.如果线程池正在停止,那么要保证当前线程是中断状态,否则要保证当前线程不是中断状态。 4.执行任务。 5.如果getTask结果为null则跳出循环,执行processWorkerExit()方法,销毁线程。

执行流程如下图所示:

图11 执行任务流程

参考:https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值