哈喽,大家好,我是老王,欢迎来到第 7 期的 Java 面试突击。
本文的面试题是,当线程池的任务溢出之后,程序会奔溃吗?
这个问题问的是关于线程池的任务数超过线程池的承载能力之后,会出现什么情况?
那么,我们本文就手撸模拟一个线程池溢出的情况,来看程序的执行情况。
涉及知识点
- 核心线程数和最大线程数有什么区别?
- 如何模拟线程池溢出?
- 拒绝策略的执行流程是什么?
- 什么是线程池的拒绝策略?
- Java 自带的拒绝策略有哪些?
- 如何自定义拒绝策略?
视频面试答案
视频内容(因为视频比较大,分成了两个视频来展示 ):
图文面试答案
当线程池的任务溢出之后,程序并不会奔溃,这时候会触发线程池的拒绝策略,Java 自带的拒绝策略有四种:
- AbortPolicy:终止策略,线程池终止执行,并直接抛出异常,Java 默认此拒绝策略;
- CallerRunsPolicy:把任务交给当前线程执行(本来是线程池自己要执行的,结果处理不过来就交给当前的主线程去处理);
- DiscardPolicy:忽略此任务(最新的任务);
- DiscardOldestPolicy:忽略最早的任务(最久的任务)。
拒绝策略的执行流程比较绕,这是因为线程池有三个重要的参数:
核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、线程池的任务队列(BlockingQueue),大部分搞不清楚核心线程数和最大线程数有什么区别?
核心线程数是指在正常情况下线程池内的线程数量;而最大线程数指的是当线程池的任务队列存储超过最大值之后,可以创建最多的线程数量。
当任务比较少的时候,线程数量会根据设置的超时时间,回归线程的数量为核心线程数量,这个时候最大线程数就暂时没用了(没有发挥的余地了)。
拒绝策略的执行流程是:当提交的任务数量大于核心线程数时,任务会被放入到线程池的任务队列中,当任务超过了最大队列值时,判断当前线程数量是否大于最大线程数,如果小于最大线程数则会新创建线程处理次任务,相反的情况下就会执行拒绝策略,如下图所示:
模拟线程池溢出
public static void main(String[] args) { ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1), new ThreadPoolExecutor.AbortPolicy()); for (int i = 0; i < 6; i++) { executor.execute(() -> { System.out.println(Thread.currentThread().getName()); }); } }
程序的执行结果如下:
pool-1-thread-2
pool-1-thread-2
pool-1-thread-1
pool-1-thread-3
pool-1-thread-4
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task org.example.App$$Lambda$1/1096979270@7cca494b rejected from java.util.concurrent.ThreadPoolExecutor@7ba4f24f[Running, pool size = 4, active threads = 4, queued tasks = 1, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at org.example.App.main(App.java:13)
从执行结果可以看出,循环在执行第 6 次就抛出异常了,这是因为最大线程数为 4,而队列最大只能存储 1 个任务,所以在第 6 个任务过来的时候,线程池已经超负荷运行了,只能执行拒绝策略了,而我们设置的拒绝策略是 AbortPolicy
所以会抛出异常。
自定义拒绝策略
除了 Java 自带的四种拒绝策略外,我们还可以自定义拒绝策略,代码如下:
public static void main(String[] args) throws InterruptedException { ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1), new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { // 添加业务处理代码 System.out.println("自定义拒绝策略"); } }); for (int i = 0; i < 7; i++) { executor.execute(() -> { System.out.println(Thread.currentThread().getName()); }); } }
以上程序执行结果如下:
自定义拒绝策略
自定义拒绝策略
pool-1-thread-1
pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-4
可以看出自定义拒绝策略,只需要重写 RejectedExecutionHandler 接口的 rejectedExecution 方法即可,可以在此方法中添加自己的业务处理代码。
小结
本文讲了线程池任务新增时的执行流程,先判断是否有空闲线程,如果的话直接执行任务,如果没有的话再判断任务队列是否是否饱和,如果不饱和把任务存储到队列中,如果饱和需要判断当前线程数是否大于最大线程数,如果小于则新增线程执行此任务,反之则执行拒绝策略。Java 提供了四种拒绝策略,你可以通过重写 RejectedExecutionHandler 接口来自定义拒绝策略。
更多执行细节和更多知识点说明,详见本文的视频部分。