前言:多线程知识是Java面试中必考的点。本文详细介绍——线程池。在实际开发过程里,很多IT从业者使用率不高,也只是了解个理论知识,和背诵各种八股文,没有深入理解到脑海里,导致面试完就忘。——码农 = 敲代码;程序员= 理解
线程池面试必考点:3大方法,7大参数,4种拒绝策略!
目录
▶ 介绍
一 . 线程池(Thread Pool)
二 . Executor、Executors 、ExecutorService 别再傻傻分不清!
▶ 逐一击穿
一 . 3大方法
二 . 7大参数
三 . 4种拒绝策略
▶ 最后
程序运行的本质就是:占用系统资源! 资源的竞争就会影响程序的运行,势必要优化资源的利用。例如:池化技术的诞生!常见的有:Java中的对象池、内存池、jdbc连接池、线程池等等
池化技术的原理:事先准备好资源,有人要用,就来我这里拿,用完再还给我!
我们知道创建、销毁线程十分浪费资源,不仅产生额外开销,还降低了计算机性能。使用线程池来管理线程,大白话总结就是:线程可复用,可控制最大并发数,线程方便管理
是为了最大化收益并最小化风险,而将资源统一在一起管理
Executors 工厂工具类
是一个工具类, 提供工厂方法来创建不同类型的线程池供大家使用!
要什么样的线程池就new什么线程池给你,相当于一个工厂!!
该工厂提供的常见的线程池类型:
// 可缓存的线程池:工作线程如空闲了60秒将会被回收。终止后如有新任务加入,则重新创建一条线程
Executors.newCachedThreadPool();
// 固定线程数池:工作线程<核心线程数,则新建线程执行任务;工作线程=核心线程数,新任务则放入阻塞队列
Executors.newFixedThreadPool();
// 单线程池:只有一条线程执行任务,按照指定顺序(FIFO,LIFO)执行,新加入的任务放入阻塞队列(串行)
Executors.newSingleThreadExecutor();
// 定时线程池:支持定时及周期性任务执行
Executors.newScheduledThreadPool();
Executor 执行者接口
线程池的顶级接口,其他类都直接或间接实现它!只提供一个execute()方法,用来执行提交的Runnable任务——只是一个执行线程的工具类!!
public interface Executor {
void execute(Runnable command);
}
ExecutorService 线程池接口
线程池接口,继承Executor且扩展了Executor【执行者接口】,能够关闭线程池,提交线程获取执行结果,控制线程的执行。可以理解为:执行者接口Executor的升级版本!
public interface ExecutorService extends Executor {
//温柔的终止线程池,不再接受新的任务,对已提交到线程池中任务依旧会处理
void shutdown();
//强硬的终止线程池,不再接受新的任务,同时对已提交未处理的任务放弃,并返回
List shutdownNow();
//判断当前线程池状态是非运行状态,已执行shutdown()或shutdownNow()
boolean isShutdown();
//线程池是否已经终止
boolean isTerminated();
//阻塞当前线程,等待线程池终止,支持超时
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
...
}
扩展:下表列出了 Executor 和 ExecutorService 的区别
Executor | ExecutorService |
---|---|
Java 线程池的核心接口,用来并发执行提交的任务 | 是 Executor 接口的扩展子接口,提供了异步执行和关闭线程池的方法 |
提供execute()方法用来提交任务 | 提供submit()方法用来提交任务 |
无返回值 | submit()方法返回Future对象,可用来获取任务执行结果 |
不能取消任务 | 可通过Future.cancel()取消pending中的任务 |
不支持关闭线程池 | 提供了关闭线程池的方法 |
指的是Executors工厂类提供的3种线程池。
上述讲解Executors工厂类说明了4种创建方式,这里只展示3种考的最多的!
a. 单线程池
public static void main(String[] args) {
// 1.定义一个单线程池:池中只有1条线程
ExecutorService pool = Executors.newSingleThreadExecutor();
// 2.定义5条线程
for (int i = 0; i < 5; i++) {
pool.execute(()->{
System.out.println(Thread.currentThread().getName()+" hi");
});
}
// 3.关闭线程池
pool.shutdown();
}
执行结果:从头到尾只有1条线程,且执行了5个任务
b. 可缓存的线程池
public static void main(String[] args) {
/** 说明:池中最大线程的创建数量为:Integer.MAX_VALUE(约等于21亿)
如果线程超过60秒没执行任务,会自动回收该线程,译为可缓存的池子 */
// 1.定义一个可缓存的线程池
ExecutorService pool =Executors.newCachedThreadPool();
// 2.定义5条线程
for (int i = 0; i < 5; i++) {
pool.execute(()->{
System.out.println(Thread.currentThread().getName()+" hi");
});
}
// 3.关闭线程池
pool.shutdown();
}
执行结果:池中被创建了5条线程执行5个任务,线程最大能创建多少条,取决于你的电脑性能
c. 固定线程池
public static void main(String[] args) {
// 1.定义一个固定长度为3的线程池
ExecutorService pool =Executors.newFixedThreadPool(3);
// 2.定义5条线程
for (int i = 0; i < 5; i++) {
pool.execute(()->{
System.out.println(Thread.currentThread().getName()+" hi");
});
}
// 3.关闭线程池
pool.shutdown();
}
执行结果:池中定义了3个线程,所以执行任务的线程最多只有3条
考点分析:线程池为什么不允许使用 Executors 工厂类 去创建!!
答:规避资源耗尽的风险。弊端如下:(不能理解的话,见下面7大参数的讲解)
FixedThreadPool 和 SingleThreadPool:阻塞队列的任务容量为 Integer.MAX_VALUE (约21亿),会堆积大量的请求导致 OOM,造成系统瘫痪。
CachedThreadPool 和 ScheduledThreadPool:最大创建线程数量为 Integer.MAX_VALUE,创建大量的线程易导致 OOM。
快速记忆:固定或单一长度的线程池,队列容量没限制!非固定的池,创建线程数没限制!
指的是自定义线程池中的7个设置参数(重点)
要点扩展:首先来查看下Executors工厂类提供的 3大方法的源码分析
// 固定长度线程池:nThreads为传入参数,最大线程数和核心线程数都为此值,固定线程数长度
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
// 单线程池:之所以只有1条线程执行,是因为核心线程数和最大线程数都是1
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
// 可缓存的线程池:最大创建数没有限制,设置了60秒空闲时间,空闲的线程超过此时间将会被回收
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
我们可以看到上述3个方法中,实际底层创建线程池都用到同一个 new ThreadPoolExecutor(),ThreadPoolExecutor是JUC提供的一类线程池工具,从字面含义来看,是指管理一组同构工作线程的资源池。通常理解为是一个自定义线程池。
来看下该类的构造参数说明:
/**
corePoolSize:核心线程数
maximumPoolSize:可创建的最大线程数
keepAliveTime:存活时间,超过此time没任务执行的线程会被回收
unit:存活时间的单位
BlockingQueue:阻塞队列
ThreadFactory:线程工厂,创建线程的,一般不用
RejectedExecutionHandler:拒绝策略
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}
corePoolSize 核心线程数量
prestartCoreThread()启动一个核心线程
和 prestartAllCoreThreads()
启动所有核心线程的方法动态调整maximumPoolSize 最大线程数
keepAliveTime TimeUnit 线程空闲时间
workQueue 任务队列
ThreadFactory 创建线程的工厂
RejectedExecutionHandler 拒绝策略
上述参数的说明太过于理论化,下面我将用生活的例子来说明重点参数的使用
模拟银行办理业务流程
由图可得出的线程池参数为:
- corePoolSize 核心线程数:2
- maximumPoolSize 最大线程数:5
- workQueue 任务队列大小:3
结论:如果核心窗口满了,则新来办理业务的人会进入排号等候区等待(阻塞队列)
思考:核心窗口和等候区都满了,那如果此时银行进来2个新办理业务的人呢?
由图可知,新进来2个人流程如下:
- 如果核心窗口被占用中(核心线程)则判断等候区 (阻塞队列)是否满了
- 排号区没满,判断等候区(阻塞队列)是否还有位置,如果有则进入等候区排队等待。
- 排号区和核心窗口都满了 (核心线程数+阻塞队列容量),那么就去判断银行的全部窗口(最大线程创建数)是否都在办理业务,如果没有,就开放2个新的窗口给新进来的2人办理业务(非核心窗口)
结论:核心线程数2 + 阻塞队列3 全部已满,如果工作线程还没达到最大线程数,线程池会给新进来的2个任务,开放创建2个新的非核心窗口来处理新任务
思考:那如果此时银行再来2个办理业务的人呢?
由图可知:
- 窗口5还没满,所以会被开放给新进来的其中一个人办理业务
- 而另外一个人,因5个窗口已全被占用,且等候区也满了,会被拒绝办理(拒绝策略)
总结,线程池的运行原理如下:
核心线程没满时,即便池中线程都处于空闲,也创建新核心线程来处理新任务。
核心线程数已满,但阻塞队列 workQueue未满,则新进来的任务会放入队列中。
核心线程数和阻塞队列都满了,如果当前线程数< 最大线程创建数, 会创建新 (非核心)线程来处理新任务
如三者都满了(核心线程数、阻塞队列、最大线程数)则通过指定的拒绝策略处理任务
优先级为:核心线程corePoolSize、任务队列workQueue、(非核心线程) 最大线程maximumPoolSize,如三者都满了,采用handler拒绝策略
指的是线程池中的最大线程数和队列都满的情况下,对新进来任务的处理方式
CallerRunsPolicy(调用者运行策略):使用当前调用的线程 (提交任务的线程) 来执行此任务
AbortPolicy(中止策略):拒绝并抛出异常 (默认)
DiscardPolicy(丢弃策略):丢弃此任务,不会抛异常
DiscardOldestPolicy(弃老策略):抛弃队列头部(最旧)的一个任务,并执行当前任务
下面我将用代码来进行演示说明:
CallerRunsPolicy(调用者运行策略)
/**
corePoolSize核心线程数:2
maximumPoolSize最大线程数:3
workQueue阻塞队列容量:2
RejectedHandler拒绝策略:CallerRunsPolicy 调用者运行
*/
public static void main(String[] args) {
// 1.自定义一个线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 3, 0
, TimeUnit.SECONDS
,new LinkedBlockingQueue(2)
, new ThreadPoolExecutor.CallerRunsPolicy()); // 指定拒绝策略
// 2.提交6个任务
for (int i = 0; i < 6; i++) {
final int num =i;
pool.execute(()->{
System.out.println(Thread.currentThread().getName()+" ok:"+num);
});
}
// 3.关闭线程池
pool.shutdown();
}
执行结果如下:
说明:首先提交了6个任务,而线程池可接收的线程容量为:队列2 + 最大创建线程数3 = 5个,因为最大线程创建数为3,所以最多只有3个线程去轮询执行5个任务,多余的第6个任务,线程池因为占满了故没办法运行,线程池指定的拒绝策略是:调用者运行策略 也就是提交任务的线程去处理,即main线程
AbortPolicy(中止策略)
与上述调用者策略的代码一致,修改ThreadPoolExecutor后面的具体策略类型即可
new ThreadPoolExecutor.AbortPolicy(); // 指定拒绝策略
执行结果如下:
说明:由于采用的是中止策略,拒绝任务并抛出异常,第6个任务因线程池已满,而无法执行。
DiscardPolicy(丢弃策略)
new ThreadPoolExecutor.DiscardPolicy(); // 指定拒绝策略
执行结果如下:
说明:第6个任务被丢弃了,结束程序运行
DiscardOldestPolicy(弃老策略)
new ThreadPoolExecutor.DiscardOldestPolicy(); // 指定拒绝策略
执行结果如下:
说明:首先核心线程是2个,故任务1、2直接进入线程池被核心线程执行,而任务3进来后,核心线程已满,则进入队列等待,任务4随后也进入队列,任务5进来后,因为核心线程和队列都已满,但还没有达到最大线程创建数3,故会创建一条非核心线程去处理任务5。此时池中已达到最大线程数,而队列也占满了,最后任务6进来,所以会抛弃最早进入队列的任务3
优秀的判断力来自经验,但经验来自于错误的判断。