击穿线程池面试题:3大方法,7大参数,4种拒绝策略

01月10日 收藏 0 评论 2 java开发

击穿线程池面试题:3大方法,7大参数,4种拒绝策略

文章申明:转载来源:https://blog.csdn.net/w15558056319/article/details/120973355

前言

多线程知识是Java面试中必考的点。本文详细介绍——线程池。在实际开发过程里,很多IT从业者使用率不高,也只是了解个理论知识,和背诵各种八股文,没有深入理解到脑海里,导致面试完就忘。
           ——码农 = 敲代码;程序员= 理解

线程池面试必考点:3大方法,7大参数,4种拒绝策略!

▶ 介绍

一 . 线程池(Thread Pool)

程序运行的本质就是:占用系统资源! 资源的竞争就会影响程序的运行,势必要优化资源的利用。
例如:池化技术的诞生!常见的有:Java中的对象池、内存池、jdbc连接池、线程池等等

池化技术的原理:事先准备好资源,有人要用,就来我这里拿,用完再还给我!

我们知道创建、销毁线程十分浪费资源,不仅产生额外开销,还降低了计算机性能。使用线程池来管理线程,大白话总结就是:线程可复用,可控制最大并发数,线程方便管理    

  1. 减少线程频繁创建和销毁的开销!
  2. 避免线程数过大导致过分调度cpu问题!
  3. 提高了响应速度!

是为了最大化收益并最小化风险,而将资源统一在一起管理

二 . Executor、Executors 、ExecutorService 别再傻傻分不清!

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 的区别

▶ 逐一击穿

一 . 3大方法

指的是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大参数

【指的是自定义线程池中的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() 启动所有核心线程的方法动态调整
  • 即使没任务执行,核心线程也会一直存活
  • 池内的线程数小于核心线程时,即使有空闲线程,线程池也会创建新的核心线程执行任务
  • 设置allowCoreThreadTimeout=true时,核心线程会超时关闭

maximumPoolSize 最大线程数

当所有核心线程都在执行任务,且任务队列已满时,线程池会创建新非核心线程来执行任务
当池内线程数=最大线程数,且队列已满,再有新任务会触发RejectedExecutionHandler策略

keepAliveTime TimeUnit 线程空闲时间

如果线程数>核心线程数,线程的空闲时间达到keepAliveTime时,线程会被回收销毁,直到线程数量=核心线程
如果设置allowCoreThreadTimeout=true时,核心线程执行完任务也会销毁直到数量=0

workQueue 任务队列
队列的说明请参考 :

ThreadFactory 创建线程的工厂
一般用来自定义线程名称,线程多的时候,可以分组区分识别

RejectedExecutionHandler 拒绝策略
最大线程数和队列都满的情况下,对新任务的处理方式,请参考下方的4种策略
【上述参数的说明太过于理论化,下面我将用生活的例子来说明重点参数的使用】

模拟银行办理业务流程


  1. 银行就相当于一个线程池
  2. 银行内部最多有5个窗口可以办理业务( maximumPoolSize 最大线程创建数),只有前2个窗口正在办理业务中(corePoolSize 核心线程数),另外3个窗口处于暂停办理
  3. 等候区只有3个位置提供排队(workQueue队列容量为:3),并且已经排满了人

由图可得出的线程池参数为:

corePoolSize 核心线程数:2
maximumPoolSize 最大线程数:5
workQueue 任务队列大小:3
结论:如果核心窗口满了,则新来办理业务的人会进入排号等候区等待(阻塞队列)

思考:核心窗口和等候区都满了,那如果此时银行进来2个新办理业务的人呢?


 由图可知,新进来2个人流程如下:

如果核心窗口被占用中(核心线程)则判断等候区 (阻塞队列)是否满了
排号区没满,判断等候区(阻塞队列)是否还有位置,如果有则进入等候区排队等待。
排号区和核心窗口都满了 (核心线程数+阻塞队列容量),那么就去判断银行的全部窗口(最大线程创建数)是否都在办理业务,如果没有,就开放2个新的窗口给新进来的2人办理业务(非核心窗口)

结论:核心线程数2 + 阻塞队列3 全部已满,如果工作线程还没达到最大线程数,线程池会给新进来的2个任务,开放创建2个新的非核心窗口来处理新任务

思考:那如果此时银行再来2个办理业务的人呢?


由图可知:
窗口5还没满,所以会被开放给新进来的其中一个人办理业务
而另外一个人,因5个窗口已全被占用,且等候区也满了,会被拒绝办理(拒绝策略)

总结,线程池的运行原理如下:


1、核心线程没满时,即便池中线程都处于空闲,也创建新核心线程来处理新任务。
2、核心线程数已满,但阻塞队列 workQueue未满,则新进来的任务会放入队列中。
3、核心线程数和阻塞队列都满了,如果当前线程数< 最大线程创建数, 会创建新 (非核心)线程来处理新任务
4、如三者都满了(核心线程数、阻塞队列、最大线程数)则通过指定的拒绝策略处理任务

优先级为:核心线程corePoolSize、任务队列workQueue、(非核心线程) 最大线程maximumPoolSize,如三者都满了,采用handler拒绝策略

三 . 4种拒绝策略

【指的是线程池中的最大线程数和队列都满的情况下,对新进来任务的处理方式】

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

▶ 最后

本文没有很深入的讲解很多源码知识,只是带着读者理解:线程池是什么?怎么用?
面试的必考点知识,基本上都是围绕着:3大方法、7大参数、4种拒绝策略 来问问题,如果你能够读懂本篇文章,基本上已经掌握了线程池的知识点!
面试造火箭,上班拧螺丝不可耻!死记硬背八股文才可耻,只有真正理解在自己脑海里的知识才是有所收获,否则你看的每一篇文章都只是别人的笔记。面试前背,面试完就忘,坚决拒绝短暂的记忆,带着自己的方式去理解它,这才是我想要表达的观点!
————————————————
优秀的判断力来自经验,但经验来自于错误的判断。

C 2条回复 评论
米线还有吗

这是我一直没记住的一个重点

发表于 2023-05-25 21:00:00
0 0
几勺奶酪

这是我一直没记住的一个重点

发表于 2022-10-21 23:00:00
0 0