一、初步认识线程池
1.使用优点
-
线程和任务分离,提升线程的的重用性。
-
控制线程的并发数量,降低服务器压力,统一管理所有线程。
-
提升系统响应速度,假如创建线程的时间为T1,执行任务的时间为T2。销毁线程的时间为T3,那么使用线程池就免去了T1和T3的时间。
2.使用场景
分别是单一线程的线程池、固定数量的线程池、周期性执行线程池、可缓存线程池
**单一线程的线程池:**一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。(newSingleThreadExecutor())
**固定数量的线程池:**创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。(newFixedThreadPool(int Threads ))
**周期性执行线程池:**创建一个支持定时及周期性的任务执行的线程池(newScheduledThreadPool(int corePoolSize) )
**可缓存线程池:**当有任务提交时,优先重复使用线程池中的空闲线程。但是如果线程池中没有空闲线程时则会直接创建新的线程执行提交的任务。 newCachedThreadPool()
二、线程池的实现
1.通过Runnable接口实现
/*
* JDK1. .5新特性,实现线程池程序
* 使用工厂类Executors中的静态方法创建线程对象,指定线程的个数
* static ExecutorService newF ixedThreadPool(int个数)返回线程池对象
* 返回的是ExecutorService接口的实现类(线程池对象)
*
* 接口实现类对象,调用方法submit (Ruunable r)提交线程执行任务
* */
public class ThreadPool {
public static void main(String[] args) {
ExecutorService ex = Executors.newFixedThreadPool(2);
ex.submit(new SubRunnable());
ex.submit(new SubRunnable());
ex.submit(new SubRunnable());
}
}
public class SubRunnable implements Runnable {
@Override
public void run() {//没有在Thread对象中,只能通过Thread类的静态方法获取当前运行线程名
System.out.println(Thread.currentThread().getName()+" Runnable+run");
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xCicJqud-1596177198483)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1596158688345.png)]
2.通过Callable接口实现
ExecutorService ex = Executors.newFixedThreadPool(2);
//返回Future对象,Callable的返回值在Future对象中
Future<String> f = ex.submit(new SubCollable());
//通过Future对象的get方法获得返回值,会要抛错
String str = f.get();
System.out.println(str);
/*
*Callable接口的实现类,作为线程提交任务出现
* 使用方法返回值
* */
public class SubCollable implements Callable {
@Override
public String call() throws Exception {//throws Exception可抛可不抛
System.out.println("Callable运行");
return "abdgljgl";
}
}
从以上的例子中我们可以看到线程是通过Executors这个类之下的静态方法创建的,然后通过submit submit方法向线程池提交所要执行的任务。实际上提交任务的常用方法还有execute,线程池使用完之后还要记得关闭掉线程池关闭的常用方法为:shoutDown与shoutDownNow。
es.shutdown();
不再接受新的任务,之前提交的任务等执行结束再关闭线程池。
es.shutdownNow();
不再接受新的任务,试图停止池中的任务再关闭线程池,返回所有未处理的线程list列表。
submit和execute分别有什么区别呢?
execute没有返回值,如果不需要知道线程的结果就使用execute方法,性能会好很多。
submit返回一个Future对象,如果想知道线程结果就使用submit提交,而且它能在主线程中通过Future的get方法捕获线程中的异常。
Future 接口设计
1. public interface Future<T> {
2. //返回计算后的结果,该方法会陷入阻塞状态,直到得到返回值
3. T get() throws InterruptedException;
4.
5. //判断任务是否已经被执行完成
6. boolean done();
7. }
三、线程实现源码分析
从线程池的实现代码中我们可以看到各类的使用情况,下面对每个类的作用和实现过程进行介绍[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CU2Z2gUa-1596177198496)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1596168077945.png)]
1.ThreadPoolExecutor类的学习研究
1.1 类构造函数中出现的变量
1. private final BlockingQueue<Runnable> workQueue; //任务缓存队列,用来存放等待执行的任务
//阻塞队列workQueue:表示如果任务数量超过核心池大小,多余的任务添加到阻塞队列中,不同的线程池 使用不同阻塞队列作为任务队列。
3. private final ReentrantLock mainLock=new ReentrantLock();
//线程池的主要状态锁,对线程池状态(比如线程池大小、runState等)的改变都要使用这个锁
5. private final HashSet<Worker> workers=new HashSet<Worker>();//用来存放工作集
6. private volatile long keepAliveTime; //线程存活时间
//线程池维护线程所允许的空闲时间, 但是线程池之后将大于核心线程池数量那部分线超过 空闲时间的线程杀死掉。
8. private volatile boolean allowCoreThreadTimeOut; //是否允许为核心线程设置存活时间
9. private volatile int corePoolSize;
//核心池的大小(即线程池中的线程数目大于这个参数时,提交的任务会被放进任务缓存队列)
11. private volatile int maximumPoolSize; //线程池最大能容忍的线程数 当队列里的任务数达到上限,并且池中正在运行的线程数小于maximumPoolSize,对于新加入的任务,新建线程。
private volatile int poolSize; //线程池中当前的线程数
13. private volatile RejectedExecutionHandler handler; //任务拒绝策略
//当队列里的任务数达到上限,并且池中正在运行的线程数等于maximumPoolSize,
//对于新加入的任务,执行拒绝策略(线程池默认的拒绝策略是抛异常)。
//JDK提供了4中拒绝策略下问我们会详细介绍
18. private volatile ThreadFactory threadFactory; //线程工厂,用来创建线程
19. private int largestPoolSize; //用来记录线程池中曾经出现过的最大线程数
21. private long completedTaskCount; //用来记录已经执行完毕的任务个数
1.2线程池拒绝策略
拒绝触发时机:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pUD3M50j-1596177198502)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1596174281359.png)]
拒绝种类:
- CallerRunsPolicy(调⽤者运⾏策略)
当触发拒绝策略时,只要线程池没有关闭,就由提交任务的当前线程处理。
⼀般在不允许失败的、对性能要求不⾼、并发量较⼩的场景下使⽤
- AbortPolicy(中⽌策略)
当触发拒绝策略时,直接抛出拒绝执⾏的异常,中⽌策略的意思也就是打断当前执⾏流程
- DiscardPolicy(丢弃策略)
直接静悄悄的丢弃这个任务,不触发任何动作
- DiscardOldestPolicy(弃⽼策略)
如果线程池未关闭,就弹出队列头部的元素,然后尝试执⾏。丢弃的是⽼的未执⾏的任务,⽽且是待执⾏优先级较⾼的 任务。基于这个特性,我能想到的场景就是,发布消息,和修改消息,当消息发布出去后,还未执⾏,此时更新的消息⼜来了 。
1.3线程池的工作队列种类
工作队列都是继承的BlockingQueue接口,它的具体实现类有
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TsHHorgh-1596177198506)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1596175270380.png)]
-
可缓存线程池:SynchronousQueue
SynchronousQueue是一个没有容量的阻塞队列。每个插入操作必须等待另一个线程的对应移除操作,反之亦然。
-
可周期性执行线程池:DelayQueue
DelayQueue封装了一个PriorityQueue,这个PriorityQueue会对队列中的ScheduledFutureTask进行排序。如果两个任务的执行时间相同,那么先提交的任务将被先执行
-
定长线程池和单一线程线程池:LinkedBlockingQueue
内部使用节点关联,会产生多一点内存占用 使用两个重入锁分别控制元素的入队和出队
1.4自定义线程池
我们是通过创建ThreadPoolExecutor对象创建自定义线程池。如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-axuPLk9h-1596177198514)(file:///C:/Users/ADMINI~1/AppData/Local/Temp/msohtmlclip1/01/clip_image001.png)]
1. 核心线程数:
核心线程数设计需要依据任务的处理时间和每秒产生的任务数量来确定,例如执行一个任务需要0.1秒系统百分之80的时间每秒都会产生100个任务,那么要想在1秒内处理完100个任务,就需要10个线程,此时我们就可以设计核心线程数为10,当然实际情况不可能这么平均,所以我们一般按照8020原则设计即可,即百分之80的情况设计核心线程数,声响剩下的百分之20可以利用最大线程数处理。
2. 任务队列长度
任务队列一般可以设计为:核心线程数/单个任务执行时间*2即可。例如上面的场景中,核心线程数设计为0.1秒,则任务队列长度设计为200.
3. 最大线程数(maximumPoolSize)
最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定:例如:上述环境中,如果系统每秒最大产生的任务是1000个,那么,最大线程数=(最大任务数-任务队列长度)*单个任务执行时间;既: 最大线程数=(1000-200)*0.1=80个;
4. 最大空闲时间(keepAliveTime)
这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可;
知道这些参数应该如何设置下面我们就可以创建自定义线程池了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RqQ001rV-1596177198519)(file:///C:/Users/ADMINI~1/AppData/Local/Temp/msohtmlclip1/01/clip_image002.png)]
2. 任务提交过程源码分析
a. 判断当前活跃线程数是否小于corePoolSize,如果小于,则调用addWorker创建线程执行任务
b. 如果不小于corePoolSize,则将任务添加到workQueue队列。
c. 如果放入workQueue失败,则创建线程执行任务,如果这时创建线程失败(当前线程数不小于maximumPoolSize时),就会调用reject(内部调用handler)拒绝接受任务。
四、线程池面试题
1. 如果你提交任务时,线程池队列已满,这时会发生什么
这⾥区分⼀下:
1)如果使⽤的是⽆界队列LinkedBlockingQueue,也就是⽆界队列的话,没关系,继续添加任务到阻塞队列中等待执⾏,因为 LinkedBlockingQueue可以近乎认为是⼀个⽆穷⼤的队列,可以⽆限存放任务
2)如果使⽤的是有界队列⽐如ArrayBlockingQueue,任务⾸先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满 了,会根据maximumPoolSize的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue继续满,那么 则会使⽤拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy
2. 高并发、任务执⾏时间短的业务怎样使⽤线程池?并发不⾼、任务执⾏时间⻓的业务怎样使⽤线程池?并发⾼、业务执⾏ 时间⻓的业务怎样使⽤线程池?
1)⾼并发、任务执⾏时间短的业务,线程池线程数可以设置为CPU核数+1,减少线程上下⽂的切换
2)并发不⾼、任务执⾏时间⻓的业务要区分开看:
a)假如是业务时间⻓集中在IO操作上,也就是IO密集型的任务,因为IO操作并不占⽤CPU,所以不要让所有的CPU闲下来,可 以加⼤线程池中的线程数⽬,让CPU处理更多的业务
b)假如是业务时间⻓集中在计算操作上,也就是计算密集型任务,这个就没办法了,和(1)⼀样吧,线程池中的线程数设置得 少⼀些,减少线程上下⽂的切换
c)并发⾼、业务执⾏时间⻓,解决这种类型任务的关键不在于线程池⽽在于整体架构的设计,看看这些业务⾥⾯某些数据是否能 做缓存是第⼀步,增加服务器是第⼆步,⾄于线程池的设置,设置参考其他有关线程池的⽂章。最后,业务执⾏时间⻓的问题, 也可能需要分析⼀下,看看能不能使⽤中间件对任务进⾏拆分和解耦。