7000字+24张图带你彻底弄懂线程池(1)

简介: 7000字+24张图带你彻底弄懂线程池(1)



今天跟大家聊一聊无论是在工作中常用还是在面试中常问的线程池,通过画图的方式来彻底弄懂线程池的工作原理,以及在实际项目中该如何自定义适合业务的线程池。

一、什么是线程池

线程池其实是一种池化的技术的实现,池化技术的核心思想其实就是实现资源的一个复用,避免资源的重复创建和销毁带来的性能开销。在线程池中,线程池可以管理一堆线程,让线程执行完任务之后不会进行销毁,而是继续去处理其它线程已经提交的任务。

线程池的好处:

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统 的稳定性,使用线程池可以进行统一的分配,调优和监控。

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

二、线程池的构造

Java中主要是通过构建ThreadPoolExecutor来创建线程池的,接下来我们看一下线程池是如何构造出来的。

线程池构造参数

  • corePoolSize:线程池中用来工作的核心的线程数量。
  • maximumPoolSize:最大线程数,线程池允许创建的最大线程数。
  • keepAliveTime:超出 corePoolSize 后创建的线程存活时间或者是所有线程最大存活时间,取决于配置。
  • unit:keepAliveTime 的时间单位。
  • workQueue:任务队列,是一个阻塞队列,当线程数已达到核心线程数,会将任务存储在阻塞队列中。
  • threadFactory :线程池内部创建线程所用的工厂。
  • handler:拒绝策略;当队列已满并且线程数量达到最大线程数量时,会调用该方法处理该任务。

线程池的构造其实很简单,就是传入一堆参数,然后进行简单的赋值操作。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

三、线程池的运行原理

说完线程池的核心构造参数的意思,接下来就来画图讲解这些参数在线程池中是如何工作的。

线程池刚创建出来是什么样子呢,如下图

不错,刚创建出来的线程池中只有一个构造时传入的阻塞队列而已,此时里面并没有的任何线程,但是如果你想要在执行之前已经创建好核心线程数,可以调用prestartAllCoreThreads方法来实现,默认是没有线程的。

当有线程通过execute方法提交了一个任务,会发生什么呢?

提交任务的时候,其实会去进行任务的处理

首先会去判断当前线程池的线程数是否小于核心线程数,也就是线程池构造时传入的参数corePoolSize。

如果小于,那么就直接通过ThreadFactory创建一个线程来执行这个任务,如图

当任务执行完之后,线程不会退出,而是会去从阻塞队列中获取任务,如下图

接下来如果又提交了一个任务,也会按照上述的步骤,去判断是否小于核心线程数,如果小于,还是会创建线程来执行任务,执行完之后也会从阻塞队列中获取任务。这里有个细节,就是提交任务的时候,就算有线程池里的线程从阻塞队列中获取不到任务,如果线程池里的线程数还是小于核心线程数,那么依然会继续创建线程,而不是复用已有的线程。

如果线程池里的线程数不再小于核心线程数呢?那么此时就会尝试将任务放入阻塞队列中,入队成功之后,如图

这样在阻塞的线程就可以获取到任务了。

但是,随着任务越来越多,队列已经满了,任务放入失败了,那怎么办呢?

此时就会判断当前线程池里的线程数是否小于最大线程数,也就是入参时的maximumPoolSize参数

如果小于最大线程数,那么也会创建非核心线程来执行提交的任务,如图

所以,从这里可以发现,就算队列中有任务,新创建的线程还是优先处理这个提交的任务,而不是从队列中获取已有的任务执行,从这可以看出,先提交的任务不一定先执行。

但是不幸的事发生了,线程数已经达到了最大线程数量,那么此时会怎么办呢?

此时就会执行拒绝策略,也就是构造线程池的时候,传入的RejectedExecutionHandler对象,来处理这个任务。

RejectedExecutionHandler的实现JDK自带的默认有4种

  • AbortPolicy:丢弃任务,抛出运行时异常
  • CallerRunsPolicy:由提交任务的线程来执行任务
  • DiscardPolicy:丢弃这个任务,但是不抛异常
  • DiscardOldestPolicy:从队列中剔除最先进入队列的任务,然后再次提交任务

线程池创建的时候,如果不指定拒绝策略就默认是AbortPolicy策略。当然,你也可以自己实现RejectedExecutionHandler接口,比如将任务存在数据库或者缓存中,这样就数据库或者缓存中获取到被拒绝掉的任务了。

到这里,我们发现,线程池构造的几个参数corePoolSize、maximumPoolSize、workQueue、threadFactory、handler我们都在上述的执行过程中讲到了,那么还差两个参数keepAliveTime和unit(unit是keepAliveTime的时间单位)没讲到,所以keepAliveTime是如何起到作用的呢,这个问题留到后面分析。

说完整个执行的流程,接下来看看execute方法代码是如何实现的。

execute方法

  • workerCountOf(c)<corePoolSize:这行代码就是判断是否小于核心线程数,是的话就通过addWorker方法,addWorker就是添加线程来执行任务。
  • workQueue.offer(command):这行代码就表示尝试往阻塞队列中添加任务
  • 添加失败之后就会再次调用addWorker方法尝试添加非核心线程来执行任务
  • 如果还是添加非核心线程失败了,那么就会调用reject(command)来拒绝这个任务。

最后再来另画一张图总结execute执行流程

四、线程池中线程实现复用的原理

线程池的核心功能就是实现了线程的重复利用,那么线程池是如何实现线程的复用呢?

线程在线程池内部其实是被封装成一个Worker对象

Worker继承了AQS,也就是有一定锁的特性。

创建线程来执行任务的方法上面提到是通过addWorker方法创建的。在创建Worker对象的时候,会把线程和任务一起封装到Worker内部,然后调用runWorker方法来让线程执行任务,接下来我们就来看一下runWorker方法。

启动线程处理任务

从这张图可以看出线程执行完任务不会退出的原因,runWorker内部使用了while死循环,当第一个任务执行完之后,会不断地通过getTask方法获取任务,只要能获取到任务,就会调用run方法,继续执行任务,这就是线程能够复用的主要原因。

但是如果从getTask获取不到方法的时候,最后就会调用finally中的processWorkerExit方法,来将线程退出。

这里有个一个细节就是,因为Worker继承了AQS,每次在执行任务之前都会调用Worker的lock方法,执行完任务之后,会调用unlock方法,这样做的目的就可以通过Woker的加锁状态就能判断出当前线程是否正在运行任务。如果想知道线程是否正在运行任务,只需要调用Woker的tryLock方法,根据是否加锁成功就能判断,加锁成功说明当前线程没有加锁,也就没有执行任务了,在调用shutdown方法关闭线程池的时候,就用这种方式来判断线程有没有在执行任务,如果没有的话,来尝试打断没有执行任务的线程。

相关文章
|
设计模式
110.【十万字带你深入学习23种设计模式】(二十二)
110.【十万字带你深入学习23种设计模式】
67 1
110.【十万字带你深入学习23种设计模式】(二十二)
|
设计模式 算法 Java
110.【十万字带你深入学习23种设计模式】(十九)
110.【十万字带你深入学习23种设计模式】
72 1
|
设计模式 存储 Java
110.【十万字带你深入学习23种设计模式】(二十五)
110.【十万字带你深入学习23种设计模式】
90 1
|
设计模式 Java 程序员
110.【十万字带你深入学习23种设计模式】(十四)
110.【十万字带你深入学习23种设计模式】
77 0
110.【十万字带你深入学习23种设计模式】(十四)
|
存储 缓存 监控
|
设计模式 Java
110.【十万字带你深入学习23种设计模式】(十六)
110.【十万字带你深入学习23种设计模式】
43 0
|
存储 监控 Java
7000字+24张图带你彻底弄懂线程池(2)
7000字+24张图带你彻底弄懂线程池(2)
|
存储 安全 Java
Java多线程基础——两万字详解
进程简单来说就是正在运行的程序,是可以通过双击执行的.exe文件,打开我们电脑的任务管理器,可以看到我们的电脑正在执行的进程,目前我们的电脑都是多进程模式。
132 0
Java多线程基础——两万字详解
|
Java 调度 Android开发
七千字带你深入JUC线程基础
七千字带你深入JUC线程基础
150 0
七千字带你深入JUC线程基础
|
消息中间件 SQL 安全
25 张图,1.4 w字!彻底搞懂分布式事务原理(三)
本文提纲如下: 0. 前言 1. 单数据源事务 & 多数据源事务 2. 常见分布式事务解决方案 2.1. 分布式事务模型 2.2. 二将军问题和幂等性 2.3. 两阶段提交(2PC) & 三阶段提交(3PC)方案 2.4. TCC 方案 2.5. 事务状态表方案 2.6. 基于消息中间件的最终一致性事务方案 3. Seata in AT mode 的实现 3.1. Seata in AT mode 工作流程概述 3.2. Seata in AT mode 工作流程详述 4. 结束语
318 0
25 张图,1.4 w字!彻底搞懂分布式事务原理(三)