type
status
date
slug
summary
tags
category
icon
password
😀
这里写文章的前言: 一个简单的开头,简述这篇文章讨论的问题、目标、人物、背景是什么?并简述你给出的答案。
可以说说你的故事:阻碍、努力、结果成果,意外与转折。
 

📝 线程池

使用线程池的好处

核心问题就是资源管理问题
  1. 频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大
  1. 对资源无限申请缺少抑制手段,容易引发系统资源耗尽的风险
  1. 系统无法合理管理内部的资源分布,会降低系统的稳定性
注意: 在 hotspot模型下,java的线程会一对一的映射内核线程,意味着每次申请和销毁都要转换到内核去操作,内核转换这个操作是十分消耗性能的,可能这个线程的时间还没 消耗+申请 加起来久
线程池的优势
  1. 提供资源的利用性: 通过池化可以重复利用已创建的线程,空闲线程可以处理新提交的任务,从而降低了创建和销毁的资源开销
  1. 提高线程的管理性: 在一个线程中管理执行任务的线程,对线程可以进行统一的创建,销毁以及监控等,对线程数做控制,防止线程无限制创建,避免线程数量的急剧上升而导致CPU过度等问题
  1. 提高程序的响应性:提交任务后,有空闲线程可以直接去执行任务,无需新建
  1. 提高系统的可扩展性:利用线程池可以更好的扩展一些功能,比如定时线程池可以实现系统的定时任务
  1. 懒惰性:先创建线程池的时候不会有任何线程,要先有第一个任务进来,才会创建线程

线程池状态

 
ctl: 对线程池的运行状态 和 线程池中有效线程的数量进行控制的一个字段
高3位保存runState,低29位保存workerCount,两个变量之间互不干扰。用一个变量去存储两个值,可避免在做相关决策时,出现不一致的情况,不必为了维护两者的一致,而占用锁资源。通过阅读线程池源代码也可以发现,经常出现要同时判断线程池运行状态和线程数量的情况。线程池也提供了若干方法去供用户获得线程池当前的运行状态、线程个数。这里都使用的是位运算的方式,相比于基本运算,速度也会快很多 利用低29位表示线程池中线程数,高3位表示线程池的运行状态
 
提交优先级
 
执行优先级
notion image
 

线程池参数

  1. corePoolSize: 核心线程数,线程池事先创建的线程,当有任务进来的时候第一时间去执行任务,空闲了也不会被回收,会一直重复这些线程
默认情况下,即使是核⼼线程也只能在新任务到达时才创建和启动。但是我们可以使⽤ prestartCoreThread(启动⼀个核⼼线程)或prestartAllCoreThreads(启动全部核⼼线程)⽅法来提前启动 核⼼线程
  1. maximumPoolSize: 最大线程数,当核心线程数处于繁忙并且队列满了的时候,会向操作系统申请额外的线程来消费新进来的任务,允许在这个池子里面最大的线程数量
  1. keepAliveTime: 除了核心线程之外的线程,当执行完任务后,存活空闲下来的时间,超过这个时间就会被回收
  1. unit: 空闲时间的单位,默认是秒
  1. workQueue:工作队列,阻塞队列.分为有界/无界,工作完的线程(不管是core,还是max线程)都会去拉取队列的任务.
  1. threadFactory:线程工厂,设置了一些标识,比如名字
  1. RejectedExecutionHandler:拒绝策略
  • 默认为Abort, 直接抛出异常(AbortPolicy)
  • 什么都不做,也不抛出一场(CallerRunsPolicy)
  • 抛出头部的任务,加入新提交的任务(DiscardOldestPolicy)
  • 谁提交的谁执行(DiscardPolicy)
 

线程池工作流程

  1. 如果当前工作线程数量小于核心线程数量,执行器总是优先 创建一个任务线程,而不是从线程队列中获取一个空闲线程
  1. 如果线程池中总的任务数量大于核心线程池数量,新接受的任务将会加入阻塞队列中,一直到阻塞队列已满。在核心线程池数量已经用完,阻塞队列没有满的场景下,线程池不会为新任务创建一个新线程
  1. 当完成一个任务执行完时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空,其中所有的缓存任务被取光
  1. 在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务
  1. 在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize,线程池就会拒绝接收任务, 当新任务过来时,会为新任务执行拒绝策略
 
notion image

ThreadPoolExecutor执行任务

有 submit 和 execute 二种方式

submit 方法

使用 FutureTask 包裹一层 Runnable,返回 FutureTask 回去,中间调用 execute 方法

execute方法

  1. 如果当前正在执行的worker数量比corePoolSize小,直接创建一个新的worker执行任务,调用addWorker方法
  1. 如果当前正在执行的worker数量大于等于corePoolSize,将任务放到阻塞队列里,等待空闲线程来执行
  1. 若队列的任务数达到上限,且当前运行线程数小于 maximumPoolSize ,任务入队列失败,新创建worker执行任务
  1. 若创建线程也失败(队列任务达到上限 且 当前线程数达到了 maximumPoolSize),对于新加入的任务,就会调用reject进行拒绝策略
 

addWorker方法

 

runWorker方法

 

getTask方法

如果发生了下面事中的一个,那么worker需要被回收:
  1. worker个数比线程池最大的还要大
  1. 线程池处于STOP状态
  1. 线程池处于SHUTDOWN状态并且阻塞队列为空
  1. 使用超时时间从阻塞队列里拿数据,并且超时之后没有拿到数据(allowCoreThreadTimeOut || wc > corePoolSize)
如果 getTask 返回的是null,那说明阻塞队列已经没有任务并且当前调用getTask的Worker需要被回收,那么会调用processWorkerExit方法进行回收
 

processWorkerExit方法

 

tryTerminate方法

 

ThreadPoolExecutor关闭

线程池关闭主要有 shutdown 和 shutdownNow 方法 shutdown方法会更新状态到SHUTDOWN,不会影响阻塞队列里任务的执行,但是不会执行新进来的任务。同时也会回收闲置的worker shutdownNow方法会更新状态到STOP,会影响阻塞队列的任务执行,也不会执行新进来的任务

shutdown方法

shutdown 方法,关闭线程池,关闭之后阻塞队列里的任务不受影响,会继续被worker处理,但是新的任务不会被接受

interruptIdleWorkers()方法

interruptIdleWorkers(boolean onlyOne)方法

shutdownNow方法

interruptWorkers() 方法

interruptIfStarted()方法

 

常用线程池

newSingleThreadPool

core=1,单线程,max=1,最大只有一条线程,keepAliveTime=0是不回收的,无界队列,一直复用同一个线程,好处是不用一直创建线程,一条线程一直复用 如果异常了,会new一条新线程,能保证顺序执行
 

newCachedThreadPool

core=0,max是Integer的最大值,阻塞队列是一个 SynchronousQueue(容量为0的队列,不能放任何东西,并且是一个阻塞队列,作用阻塞任务,让缓存任务队列无效,所有的任务都是走非核心线程,效率非常高),没有池化思想了,一个任务创建一个线程,回收时间为 60 s,当没任务执行/提交的时候,线程池内无线程
  • 时效性高,新任务进来就会有线程去执行任务
  • 不用管理线程的回收,因为线程池管理了线程的回收(keepAliveTime的时间为60s)
  • 资源损耗严重
 

newFixedThreadPool

core 和 max 的值都是相等,不会额外的创建新线程,keepAliveTime=0,也不会回收空闲线程,无界队列,任务都由核心线程数去执行,保证线程的复用 特点: 适用于,已知任务数量,但是比较耗时的任务 注意: newFixedThreadPool的阻塞队列大小是没有大小限制的,如果队列堆积数据太多会造成资源消耗

newScheduledThreadPool

线程池支持定时任务及周期性执行任务,创建一个 corePoolSize 传入,最大线程数为整形的最大数的线程池
 

newWorkStealingPool

建持有足够线程的线程池来达到快速运算的目的,在内部通过使用多个 队列 来减少各个线程调度产生的竞争。这里所说的有足够的线程 JDK 根据 当前线程的运行需求向操作系统申请足够的线程,以保障线程的快速执行,并很大程度 地使用系统资源,提高并发计算的效率,省去用户根据 CPU 资源估算并行度的过程 然,如果开发者想自己定义线程的并发数,则也可以将其作为参数传入
 

动态线程池

动态化线程池的核心设计包括如下:
  1. 简化线程池的配置:线程池构造参数有8个,但是最核心的是3个,corePoolSize,maximumPoolSize,workQueue,它们最大程度的决定了线程池的任务分配和线程的分配策略。考虑到在实际应用中我们获取并发行的场景
  • 并行执行子任务,提高响应速度。这种情况下应该使用同步队列,没有什么任务应该被缓下来,而是应该立即执行
  • 并行执行大批次任务,提高吞吐量。这种情况下,使用使用有界队列,使用队列取缓冲大批量的任务,队列容量必须声明,防止任务无限制堆积。所以线程池只需要提供这三个关键参数的配置,并且提供两种队列的选择,就可以满足绝大多数的业务需求,Less is More
  1. 参数可动态的修改:为了解决参数不好配,修改参数成本高等问题,在Java线程池留有高扩展性的基础上,封装线程池,允许线程池监听同步外部的消息,根据消息进行修改配置。将线程池的配置放置在平台侧,允许开发同学简单的查看、修改线程池配置。
  1. 增加线程池监控,对某事物缺乏状态的观测,就对其改进无从下手。在线程池执行任务的生命周期添加监控能力,帮助开发同学了解线程池状态

🤗 总结归纳

📎 参考文章

 
💡
有关文章的问题,欢迎您在底部评论区留言,一起交流~