文章目录
为什么是需要使用线程池?
如果直接创建线程,存在以下问题:
1、反复创建线程系统开销比较大,每个线程的创建和销毁都需要时间,如果线程执行的任务比较简单,那么创建销毁线程占用资源比线程本身执行任务销毁的资源还要多。
2、过多的线程会消耗更多的内存,也会造成过多的上下文切换,使得系统不稳定。
使用线程池优点:
1、线程池可以控制线程生命周期的开销问题,同时加快响应速度。线程池的线程是可以复用的,通过少量的线程执行大量的任务。
2、线程池可以统筹内存和CPU的使用,避免资源使用不当。线程池可以根据配置和任务数量灵活控制线程数量。
3、线程池可以统一管理资源。
线程池各个参数的含义
属性 | 名字 | 作用 |
---|---|---|
corePoolSize | 核心线程数 | 当线程池运行线程少于corePoolSize时,将创建一个新的线程来处理请求,即使其他线程处于空闲状态 |
threadFactory | 线程工厂 | 用于创建线程的工厂 |
workQueue | 队列 | 用于保留任务并移交工作线程的阻塞队列 |
maximunPoolSize | 最大线程数 | 线程池允许开始最大线程数 |
handler | 拒绝策略 | 在线程池添加任务时候,两种情况下触发拒绝策略:1、线程池运行状态不是RUNNING;2、线程池达到最大线程数,并且阻塞队列已经满。 |
keepAliveTime | 保持存活时间 | 当线程池线程数大于核心线程数时,多余的线程空闲时间超过keepAliveTime时会被终止。 |
线程创建时机
线程池状态
RUNNING:接受新任务并处理排队的任务
SHUTDOWN:不接受新任务,但处理排队的任务
STOP:不接受新任务,不处理排队的任务,并且中断进行中的任务
TIDYING:所有的任务都已经终止,workerCount为零,线程转换到TIDYING状态将运行terminated()钩子方法。
TERMINATED:terminated()已经完成
线程池状态转换
线程池有哪些拒绝策略
拒绝时间
1、线程池不在RUNNING状态,线程池调用shutdown等方法关闭线程池后,即便线程池内部依然存在没有执行完的任务正在执行,但是线程池已经关闭,此时再向线程池内提交任务,就会遭到拒绝。
2、线程池没有能力继续处理新提交的任务。线程池达到最大线程数,且队列已经满的情况。
4种拒绝策略
AbortPolicy:在拒绝任务时,会抛出异常。(异常:RejectedExecutionException 的 RuntimeException)
DiscardPolicy:当提交新任务时候,直接抛弃掉,不做任何通知。
DiscardOldestPolicy:当提交新任务时候,丢弃掉存活时间长的队列头任务,腾出空间给新的任务
CallerRunsPolicy:当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。
阻塞队列
ArrayBlockingQueue:基于数组结构的有界阻塞队列,按先进先出对元素进行排序。
LinkedBlockingQueue:基于链表结构的有界/无界阻塞队列,按先进先出对元素进行排序,吞吐量通常高于 ArrayBlockingQueue。Executors.newFixedThreadPool 使用了该队列。
SynchronousQueue:不是一个真正的队列,而是一种在线程之间移交的机制。要将一个元素放入 SynchronousQueue 中,必须有另一个线程正在等待接受这个元素。如果没有线程等待,并且线程池的当前大小小于最大值,那么线程池将创建一个线程,否则根据拒绝策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是被放在队列中,然后由工作线程从队列中提取任务。只有当线程池是无界的或者可以拒绝任务时,该队列才有实际价值。Executors.newCachedThreadPool使用了该队列。
PriorityBlockingQueue:具有优先级的无界队列,按优先级对元素进行排序。元素的优先级是通过自然顺序或 Comparator 来定义的。
线程池线程数多少合适?
线程池处理的任务通常分为cpu密集型任务、IO密集型任务。
cpu密集型任务核心线程数设置
加密、解密、压缩、计算等一系列需要大量耗费 CPU 资源的任务
核心线程数
这样的任务最佳的线程数为 CPU 核心数的 1~2 倍
原因
因为计算任务非常重,会占用大量cpu资源,此时每个核心线程都是满负荷工作。设置过多的线程数会造成不必要的上下文切换。
IO密集型任务核心线程数设置
数据库、文件的读写,网络通信等任务,这种任务的特点是并不会特别消耗 CPU 资源,但是 IO 操作很耗时,总体会占用比较多的时间。
核心线程数
最大线程数一般会大于 CPU 核心数很多倍
线程数 = CPU 核心数 *(1+平均等待时间/平均工作时间)
例如我们有个定时任务,部署在4核的服务器上,该任务有100ms在计算,900ms在I/O等待,则线程数约为:4 * 1 * (1 + 900 / 100) = 40个。
原因
因为 IO 读写速度相比于 CPU 的速度而言是比较慢的,如果我们设置过少的线程数,就可能导致 CPU 资源的浪费。而如果我们设置更多的线程数,那么当一部分线程正在等待 IO 的时候,它们此时并不需要 CPU 来计算,那么另外的线程便可以利用 CPU 去执行其他的任务,互不影响,这样的话在任务队列中等待的任务就会减少,可以更好地利用资源。
使用线程池队列需要注意什么?
有界队列:需要注意线程池满后。被拒绝的任务如何处理。
无界队列:如果提交任务大于线程池处理任务的速度时候 ,可能导致内存溢出。
线程只能在任务到达时才启动吗?
默认情况下,即使是核心线程也只能在新任务到达时才创建和启动。但是我们可以使用 prestartCoreThread(启动一个核心线程)或 prestartAllCoreThreads(启动全部核心线程)方法来提前启动核心线程。
核心线程怎么实现一直存活?
|
| 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
| — | — | — | — | — |
| 插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
| 移除 | remove() | poll() | take() | poll(time,unit) |
| 检查 | element() | peek() | 不可用 | 不可用 |
核心线程在获取任务时,通过阻塞队列的 take() 方法实现的一直阻塞(存活)。
非核心线程如何实现在 keepAliveTime 后死亡?
原理同上,也是利用阻塞队列的方法,在获取任务时通过阻塞队列的 poll(time,unit) 方法实现的在延迟死亡。
非核心线程能成为核心线程吗?
虽然我们一直讲着核心线程和非核心线程,但是其实线程池内部是不区分核心线程和非核心线程的。只是根据当前线程池的工作线程数来进行调整,因此看起来像是有核心线程于非核心线程。
如何终止线程池?
终止线程池主要有两种方式:
shutdown:“温柔”的关闭线程池。不接受新任务,但是在关闭前会将之前提交的任务处理完毕。
shutdownNow:“粗暴”的关闭线程池,也就是直接关闭线程池,通过 Thread#interrupt() 方法终止所有线程,不会等待之前提交的任务执行完毕。但是会返回队列中未处理的任务。
线程池ctl
ctl怎么设计的?
ctl是打包两个概念字段的原子整数。
workerCount:指示线程的有效数量。
runState:线程池状态:RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED
ctl int类型、32位。高3位为runState的,低29位为workerCount。
例如,当我们的线程池运行状态为 RUNNING,工作线程个数为3,
则此时 ctl 的原码为:1010 0000 0000 0000 0000 0000 0000 0011
为什么要这样设计?
runState 和 workerCount 是线程池正常运转中的2个最重要属性,线程池在某一时刻该做什么操作,取决于这2个属性的值。
因此无论是查询还是修改,我们必须保证对这2个属性的操作是属于“同一时刻”的,也就是原子操作,否则就会出现错乱的情况。如果我们使用2个变量来分别存储,要保证原子性则需要额外进行加锁操作,这显然会带来额外的开销,而将这2个变量封装成1个 AtomicInteger 则不会带来额外的加锁开销,而且只需使用简单的位操作就能分别得到 runState 和 workerCount。
由于这个设计,workerCount 的上限 CAPACITY = (1 << 29) - 1,对应的二进制原码为:0001 1111 1111 1111 1111 1111 1111 1111(不用数了,29个1)。
通过 ctl 得到 runState,只需通过位操作:ctl & ~CAPACITY。
(按位取反),于是“CAPACITY”的值为:1110 0000 0000 0000 0000 0000 0000 0000,只有高3位为1,与 ctl 进行 & 操作,结果为 ctl 高3位的值,也就是 runState。
通过 ctl 得到 workerCount 则更简单了,只需通过位操作:c & CAPACITY。