线程池的设计与实现解析
- 概述
- 前言:
主要分享基于ThreadPoolExecutor实现的线程池设计和实现.
-
- 编写目的
- 介绍线程相关的基础知识;
- 介绍线程池的原理,及如何合理使用线程池;
- 适用人群
- 有Java基础,了解并发的开发人员;
- 基础知识介绍
- 锁
2.1.1 Synchronized
ART VM原理简述:
获取/释放锁的过程实际上,由monitor对象进行系统调用(monitorEnter/exit),从而进行获取和释放锁的操作;
Hot Spot VM Synchronized内部优化:
- 偏向锁(Biase Lock): 用于单线程的情况;
- 轻量级锁(Light weight Lock): 用于同一时间没有发生锁竞争的情况(前期通过自旋的方式获取锁);
- 重量级锁(Heavy weight Lock): 用于有发生锁竞争的情况;
Art VM Synchronized内部优化:
- 瘦锁(ThinLock): 开始的时候,会尝试通过自旋方式,进行锁的获取;如果尝试多次后,会进行锁膨胀(inflate)为FatLock;
- 胖锁(FatLock): 如果是非持有锁线程获取锁(通过Futex的方式)将线程挂起;
ART VM的Synchronized获取锁实现流程图:
用法:
①void Synchronized testLock1 () {}//方法级
②Void testLock2 () {//代码块
Synchronized(xxx.class) {//class对象为锁对象
//do something
}
}
③Void testLock2 () {//代码块:
Synchronized(obj) {//实例对象为锁对象
//do something
}
}
2.1.2 Lock
原理简述:
Java.util.concurrent并发包下基于Java语言实现的锁,其实现都是基于AQS(CAS+volatile+LockSupport).
分类:
- 乐观锁/悲观锁
乐观锁: 可以理解成锁很块就会被释放,并获取到,所以会做轮询的实现;
悲观锁: 可以理解成锁不能马上获取,所以会进行系统调用,进行线程挂起操作;
- 独占锁/共享锁/可重入锁
独占锁: 同一时间,只能一个线程获取锁;
共享锁: 同一时间,多个线程可获取锁, 如:ReentrantReadWriteLock.readlock;
可重入锁: 统一线程,可多次获取同一个锁, 如:ReentrantLock;
用法:
Lock.lock();
try{
//同步代码块
}finally{
Lock.unlock();
}
-
- 阻塞队列(BlockingQueue)
实现原理简述: 是基于lock+condition实现的阻塞队列;
2.2.1 类图
简单来说,阻塞队列本质上还是用于盛放元素的集合,只是通过组合的方式,引入了Lock和Condition,从而让队列具备了支持生成者-消费者模型的功能;
2.2.2 代码实现简析
阻塞队列的put/take代码示例:
-
- Java线程模型
简述:在ART vm实现中,每个Java的Thread通过系统调用pthread_create创建线程;
-
- 生产者消费者模型简析
举个悲伤的栗子:
生产者: 快递员;
消费者: 收件人;
缓存区: 丰巢;
- 时间:双十一之后;
- 地点:金山园区;
- 人物:快递小哥和一群在双十一剁手的同学;
- 事件:快递小哥让一群有快递的同学,现在去园区领取快递;
- 快递领取方式:杯具的是,快递小哥为了让自己方便并且无误的发快递,因此在现场叫一个名字,然后过来一个人取一个快递;(你还不能走,因为走了就默认没人认领,明天继续让帮你过来以这样的方式去快递)
- 问题分析:因为快递小哥只有一个人,他只能一个个的喊名字,而快递却又很多,你很可能需要在现场等好久都没叫到你的名字,导致你浪费时间也不得不留在原地等待,晚上还有加班改bug;
- 效率改进:因为你要赶着回去接bug,所以你建议快递小哥,讲快递放到丰巢,等自己有时间自己去取;这样双方都不需要等待对方,会提高你和快递小哥双方的效率;
(另外,网络的数据包的传输也是符合生产者-消费者模型的)
- ThreadPoolExecutor的设计与实现
3.1 架构图
架构图解析(线程池的任务处理规则):
- 一个任务通过execute放进线程池;
- 首先会看看corePoolSize是否满(图中箭头1),如果没有,直接创建worker线程,执行该任务;
- 其次会看下BlockingQueue是否满(图中箭头2),如果没有,该任务直接放入BlockingQueue中,等待worker线程从BlockingQueue取出并执行;
- 再次会查看maxPoolSize是否满(图中箭头3),如果没有,会直接创建worker线程,并执行该任务;
- 最后,如果上述条件都不满足,则会执行Reject策略(图中箭头4),默认的实现是会丢弃任务;
3.2 类图
3.3.代码简析
流程图:
①提交任务代码流程:
分析:这里的任务处理策略,详细请看注释部分的代码;
②创建worker线程代码分析:
分析:在创建worker线程的时候,或拿到全局锁(mainLock),以保证并发安全;
③Worker任务处理代码分析:
分析:worker线程会轮询式的从阻塞队列获取任务并进行处理;
- 配置线程池参数
参数配置原则:
- 计算性的操作,建议设置接近于CPU核数的线程数量,否则会有非常多的线程上下文切换导致的额外开销,建议:N+1;
- IO阻塞想的操作,建议设置多余CPU核数的线程数量,否则线程阻塞会导致CPU空闲带来的CPU浪费,建议:2N+1;
- 混合性的操作,线程数量介于两者之间;
- 合理中断任务
核心原理:实际上Java是没有直接stop Thread的方法(之前的Thread.stop已经不支持了),因此Java都是通过中断的方式来进行取消/结束任务的;
线程中断分为两种情况:
- 阻塞的线程(如:Object.wait/LockSupport.park),可以通过Thread.interrupt来唤醒阻塞的线程,并抛出InterruptedException让程序员处理;
- 非阻塞线程,可以通过Thread.interrupt调用,然后判断Thread.isInterrupt来判断中断的标识符是否为true;
注:InterruptedException的异常是vm抛出的(因为Object.wait实际上是调用的Monitor::Wait来实现的线程阻塞),如图:
线程池中提供了shutdown/shutdownNow来关闭线程池,区别如下:
- Shutdown是运行阻塞队列先前的任务,并关闭空闲线程:
- shutdownNow是关闭线程,并不会再执行剩余的任务:
- Executors的简单分析+涉及到的几种线程池
- newFixedThreadPool:固定线程处理任务;
- newSingleThreadExecutor:单个线程处理任务;
- newCachedThreadPool:来一个任务,创建一个线程处理.
- newScheduledThreadPool:定时的处理任务;
- newWorkStealingPool:工作窃取(参考之前的技术分享);
总结:很多线程池其实都是ThreadPoolExecutor实现的,只是配置参数不同罢了;
8. 遇到的问题(futuretask将异常吞掉的问题)
8.1 现象描述:
- 将一个Runnable 任务通过 submitOnComputationThread 到线程池中;
- Runnable内部有exception抛出,但是 没有任务异常堆栈信息。
8.2 问题分析:
FutureTask.java
run()捕获了异常,并最终通过 setException,设置到outcome中;
8.3 结论:
1.futureTask内部的run()方法,将异常截获,并通过setExcption保存异常结果;
2.需要通过 FutureTask 的get()方法,才能知道任务的失败获成功;
3.futureTask不管成功失败,都会最终调用 done()方法;
8.4 解决方案:
1.可以重写FutureTask 的done( ),在这里调用get(),即可把异常抛出;
2.重写 setException,直接抛出异常信息;