目录
线程池
需要异步操作的时候往往是把任务提交到线程池中
我们来详细分析下线程池
线程数的设置
CPU密集型
CPU核数+1
+1是避免线程出现中断或等待,空闲出来的CPU可以继续执行新的线程
这样设置是避免线程切换带来的性能损耗
因为最大处理能力就是CPU的核数
IO密集型
IO等待时间越长,可以设置的线程数越多
依据是IO的等待时间
因为线程进入等待会交出CPU,CPU此时去执行其他任务即可
理论公式
这个是在“实验室环境下的理论公式”
而耗时可以从各种运维工具中获得,最简单的就是jvisualvm
实际最佳线程还是需要进行压测
核心线程和最大线程的设置
如果一直调用很高,那么直接设置一致,让线程池直接进入最大负载运行
如果偶尔调用很高,那么就可以少设置点核心线程,共低调用时使用,设置最佳线程作为高调用时使用。这样可以节省资源。
这里说的调用是线程池提交的任务调用
阻塞队列设置
这个可以参考上一节,选择下那个合适
但是一定要设置队列容量
避免提交任务过多而引起oom
而且如果任务提交的过多
晚提交的任务可能还没执行完或者还没有执行结束前端就已经超时了
我们可以看到如果设置的太大而系统处理不了的话最后提交的任务可能需要5s才能执行完
极端情况下可能线程池永远都没办法在期望的时候处理完任务
应该根据自己业务预估下承载能力
多余的任务交给拒绝策略来处理
线程池执行
创建核心线程
JDK自带的线程池构造方法new对象是没有创建线程的
只有在提交任务的时候会新增核心线程
第一步判断是否到达核心线程数
没有就创建后执行
核心线程创建够了
继续提交任务则进入等待队列
线程会在空闲后获取任务继续执行
如果有核心线程已经执行完任务了
则调用出队方法
因为是阻塞队列所以会阻塞在出队方法上
等待任务进入等待队列就会唤醒线程执行
队列满了判断最大线程数
此时如果线程数还没有到达最大线程数
则继续创建线程执行
最大线程数也够了
进入拒绝策略
线程池线程关闭
正常超时淘汰
实际情况比较复杂我们可以想一下所有任务都执行完了,并且此时线程数量大于核心线程数
现在线程池中的线程都阻塞在阻塞队列的出队方法
这个方法是有超时时间的,如果到超时时间还没有被唤醒,那就说明要进入超时淘汰了
因为线程是依靠循环来保持活跃,那么所有待淘汰线程在循环的时候对全部线程数CAS -1 成功的线程不再循环,就会自然淘汰了(如下图)
核心线程也可以开启超时淘汰allowCoreThreadTimeOut(true)
执行异常线程
原线程会推出
但是会在processWorkerExit方法中会替补一个线程(核心、非核心都会补充)
这里可能会有疑问:为什么要这么麻烦?一个丢掉在创建一个?
因为这里其实是希望把异常提交到主线程,让主线程有所感知,而不是内部消化掉,如果不希望抛出线程,需要业务方自己处理
线程池关闭
执行shutdown() 或者 shutdownNow() 则阻塞方法会抛出中断异常
在中断异常处理中会把超时标志位改为已超时,则进入超时淘汰
线程池状态
五个状态
状态 | 解释 |
---|---|
Running | 接受新的任务且会处理队列中的任务 |
Shutdown | 不能接受新的任务,会处理队列中的任务,执行完后停掉所有线程 |
Stop | 不能接受新的任务,不会处理队列中的任务,停掉所有线程 |
Tidying | 在最后一个线程停掉后CAS线程池状态为此状态,并且执行terminated(),执行完后改为Terminated |
Terminated | 线程池为0且执行完terminated() |
状态流转:
ctl
线程池的状态,原子int类型
此变量32位,前3位表示状态,后29位表示池内线程个数
状态 | 比特位 |
---|---|
Running | 111 |
Shutdown | 000 |
Stop | 001 |
Tidying | 010 |
Terminated | 011 |
源码分析
添加任务
如果workerCount < 核心线程数
- 则addWorker(此任务,true),如果成功直接返回(跳出整个方法),失败则重新获取ctl并继续
如果状态为running且入队成功(offer返回true)
- 则重新获取ctl
判断如果不是running装填并且移除此次提交的任务成功,进入拒绝
或者判断workerCount==0, 则addWorker(null, false)
如果状态非running或者入队失败(满了),则判断addWorker(此任务,false)是否失败
失败则进入拒绝
这个源码就验证了提交任务处理顺序为
- 核心线程、不关心是否空闲
- 入队
- 最大线程
- 拒绝
addWorker
addWorker(此任务,true)
设置跳出位:retry:
循环
如果状态为SHUTDOWN或者之后
且 !(状态为SHUTDOWN 且 此任务==null 且 阻塞队列不为空)
这里因为此任务不为null则只有有1种情况
池需要关闭了 就返回false了
嵌套循环
获取工作线程
如果大于最大任务数 或者 大于核心线程数
则整体返回false
否则CAS +1 工作线程数
成功则跳至跳出位retry
失败则读取状态位是否变更
变更了就跳至跳出位retry 没有就重新从跳出位retry开始循环
真实创建线程
红框1:创建线程
红框2:判断是否线程是否可以运行任务
如果线程池是running
或者线程池是shutdown firstTask是空的
:这里场景是虽然shutdown了但是队列里还有任务所以需要新增
加入workers
更新最大PoolSize
设置added标志为true
红框3:执行线程run方法
- 循环获取任务 如果不为空就执行task
- 执行前扩展
- 执行后扩展
- 执行任务
获取任务
1和3配合如果工作线程比最大线程还多了那就需要-1
CAS -1 成功了就返回null 导致线程跳出while循环 自然-1
失败了就continue重新判断
2就是检查是否超时
如果需要超时则使用带时间的出队,超时了就返回null 线程自然-1
如果不需要超时则使用不带时间的出队,等着任务提交唤醒
线程池中断
在调用shutdown或者shutdownNow方法
会直接调用所有线程的阻断方法,设置中断信号
如果线程在执行任务,则执行完后重新获取任务的时候直接返回null跳出
如果线程在阻塞,直接排除异常,走上边的timeout流程跳出
shutdown或者shutdownNow方法有什么区别
shutdownNow是所有线程都会中断,有返回队列剩余的任务
shutdown是中断空闲线程,没有返回值
怎么判断空闲呢?
worker自身就是AQS实现,自己就是一把锁
在执行任务前,自己加锁
执行任务后,自己解锁
那再判断的时候可以尝试加锁,如果加锁成功就是空闲线程
AQS的state什么用
state再worker中有三个状态
-1:初始化(new Worker() 但是还没有开始准备拿任务执行)
1:加锁(lock():CAS 从0 到 1)
0:解锁(unlock():setState(0))
这里我们有疑问:加锁是从0到1 那怎么从初始化到加锁成功呢
线程池的线程在执行run() 的时候先调用一次unlock() 再加锁
这样就能控制线程的各个状态
-1就是刚初始化还没有调用run()方法的线程
1就是正在执行任务的线程
0就是空闲线程
mainLock是什么用
我们在之前知道了worker会存放到 workers中
workers是一个HashSet 不能保证线程安全
所有就借助mainLock来加锁