线程池何时触发拒绝策略
- 首先咱们了解一下,线程池在处理节点中,什么条件下会触发拒绝策略。咱们来看一下这张图
线程池运行时序图
- 根据咱们对线程池源码的了解,咱们绘出的线程池运行时序图如上,可以看出,线程池在核心线程数已满,阻塞队列已满,并且线程数已达最大线程数时会立马触发我们创建线程池时所设定的拒绝策略。
- 那么这个地方有个非常有意思的点,不知道大家有没有留意,其实这个也能让咱们更好地理解线程池的设计原理。
Q:为什么线程池要先放阻塞队列,而不是先判断达到最大线程数后再放入阻塞队列?
A:因为假如咱们设计让线程池先判断最大线程数,后放置阻塞队列,就会出现如下问题,
当瞬时提交的任务量大,线程池创建线程达最大线程数后才放置阻塞队列,那么在任务提交
量下来后,将会存在大量的空闲线程(虽然咱们设置了线程最大空闲时间,
但依然会有一段时间存在),那么此时就会出现大量的线程上下文切换,
线程上下文切换是非常浪费资源的,那么线程池反而会降低咱们的线程使用效率。
线程池提供的四种拒绝策略,触发时都具体做了哪些事情
- 首先我们用一段伪代码来验证下四种拒绝策略是不是按照名称含义来工作的
public class ThreadPoolDemo {
public static final ThreadPoolExecutor THREAD_POOL_EXECUTOR =
new ThreadPoolExecutor(1,1,1,
TimeUnit.SECONDS,new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.CallerRunsPolicy());
public static void main(String[] args) {
//thread first
THREAD_POOL_EXECUTOR.submit(()->{
//do nothing,just sleep 100s
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("已提交第一个任务..");
//thread second
THREAD_POOL_EXECUTOR.submit(()->{
//do nothing,just sleep 100s
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("已提交第二个任务..");
System.out.println("线程池资源即将耗尽..");
long startTime = System.currentTimeMillis();
//thread third
THREAD_POOL_EXECUTOR.submit(()->{
//do nothing,just sleep 100s
try {
System.out.println("触发拒绝策略,该方法将由主线程同步执行");
// System.out.println("触发拒绝策略,等待最久的一个任务将被丢弃,当前任务数量:" + THREAD_POOL_EXECUTOR.getTaskCount());
// System.out.println("触发拒绝策略,将直接报错");
// System.out.println("触发拒绝策略,该线程将被直接丢弃");
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("主线程已经被阻塞,但线程三已执行完,共计耗时:" + (System.currentTimeMillis() - startTime));
// System.out.println("线程池已将等待最久一个任务丢弃,我会被立马执行" + (System.currentTimeMillis() - startTime));
// System.out.println("线程池会报错,我将无法执行");
// System.out.println("线程池将直接丢弃任务三,我会立马被执行" + (System.currentTimeMillis() - startTime));
}
}
- 对代码简要说明下,为了更好进行验证,咱们构造了一个核心线程数、阻塞队列大小、最大线程数都是1的一个线程池,然后咱们利用Lambda表达式免去创建线程的步骤,然后我们创建三个线程,根据线程池原理,当我们提交第三个任务时将触发拒绝策略。
插播一则通告:本人在代码一线工作近八年时间,有非常丰富的面试经验,有需要优化简历,模拟面试的同学可以联系即时沟通号:xiaolang1530368931。将简历优化成大厂面试官想看的,提前回答大厂面试官可能会问的问题,为进大厂做最后的冲刺。
咱们先来看CallerRunsPolicy,即调用主线程来执行,运行效果如下:
已提交第一个任务..
已提交第二个任务..
线程池资源即将耗尽..
触发拒绝策略,该方法将由主线程同步执行
主线程已经被阻塞,但线程三已执行完,共计耗时:100017
- 咱们会发现最后一句并没有立即输出,并且根据耗时看出几乎与第三个任务执行的时间一致,那这是为什么呢?线程池又是如何调用主线程去执行任务的呢?何解?唯有源码解惑也!源码具体实现如下:
/**
* Executes task r in the caller's thread, unless the executor
* has been shut down, in which case the task is discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
- 看到源码,有没有一种恍然大悟的感觉,精妙吧,咱们简要理解下,Runnable 这个对象不就是咱们刚才在main方法中使用Lambda表达式创建的线程三么(此处麻烦大家花点宝贵的时间跟下源码,因文章内容有限,此处不做展开),而且这个地方是直接调用了Runnable的run方法,说明这个地方没有开启线程,而是一种同步调用,也就是虽然使用了线程池,但是实际上是由主线程同步调用线程三中的Thread.sleep(100000),这就解释了为什么咱们最后那句话等了这么久才输出,相当于在其输出之前,线程沉睡了100s。
- 经过以上分析,如果我们创建线程池时指定的拒绝策略是CallerRunsPolicy,当触发拒绝策略时,就是转变为当前主线程将提交的任务同步完成,才能继续后面的业务操作,也就是表现为线程池调用了主线程执行提交的任务。
咱们再来看DiscardOldestPolicy,即将等待最久的任务丢弃,运行效果如下:
已提交第一个任务..
已提交第二个任务..
线程池资源即将耗尽..
线程池已将等待最久一个任务丢弃,我会被立马执行0
触发拒绝策略,等待最久的一个任务将被丢弃,当前任务数量:2
- 根据运行结果,最后一句竟然优先于线程三输出?不是说会移除等待最久的一个任务吗?那线程三为什么没有被立即执行呢?带着困惑咱们来看下如下源码:
/**
* Obtains and ignores the next task that the executor
* would otherwise execute, if one is immediately available,
* and then retries execution of task r, unless the executor
* is shut down, in which case task r is instead discarded.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
- 咱们发现源码确实是调用了阻塞队列的poll方法,即弹出队首元素并移除该元素,然后再调用execute方法,即再重新提交该任务,按照源码思路,确确实实将等待最久的任务直接丢弃,那为啥咱们的任务三并没有执行呢?这个时候咱们回过头来看下咱们创建的线程池,咱们可是corePoolSize与maximumPoolSize都是设置的1呀。也就是自始至终只有一个线程在跑,那么咱们线程池确实是把等待最久的任务丢弃了,但是这个时候线程一还没执行完,所以线程池是把咱们新提交的任务又放回了阻塞队列等待执行了,所以才出现了线程三中的输出在最后一句之后。
接下来AbortPolicy和DiscardPolicy我们就简单过下,确实童叟无欺
- AbortPolicy运行效果如下:
已提交第一个任务..
已提交第二个任务..
线程池资源即将耗尽..
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@7c0e2abd rejected from java.util.concurrent.ThreadPoolExecutor@5ae9a829[Running, pool size = 1, active threads = 1, 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 java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:112)
at com.xx.utils.prefix.thread.ThreadPoolDemo.main(ThreadPoolDemo.java:47)
- 源码如下(一触发拒绝策略,直接抛错):
/**
* Always throws RejectedExecutionException.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
* @throws RejectedExecutionException always
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() +
" rejected from " +
e.toString());
}
- DiscardPolicy运行效果如下:
已提交第一个任务..
已提交第二个任务..
线程池资源即将耗尽..
线程池将直接丢弃任务三,我会立马被执行0
- 源码如下(就是虽然提交了任务,但线程池什么都没干,也就是直接丢弃):
/**
* Does nothing, which has the effect of discarding task r.
*
* @param r the runnable task requested to be executed
* @param e the executor attempting to execute this task
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
伪代码运行完没自动退出
- 接下来咱们看一个额外有意思的点,如果有同学真实跑过代码会发现,就算线程池已经把所有事情都做完了,但是main方法并没有结束,也就是JVM并没有退出,这个大家好奇不,欸,奇怪的知识又增加了。
- 咱们跟源码会发现线程池是通过维护Worker(实现了Runnable,也就是一个线程对象),Worker的创建如下(咱们本次不关心线程池运行原理,后面咱们有时间再慢慢分析线程池原理,所以没有看过源码的同学不用太在意,咱们关注线程创建方式就行):
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
- 根据源码Worker中的thread是通过getThreadFactory().newThread(this)创建的,并把自己作为Runnable传入了thread,然后咱们再根据咱们创建线程池时使用的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}
- 会发现getThreadFactory()得到的对象是Executors.defaultThreadFactory()即DefaultThreadFactory,然后调用其newThread方法如下:
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
- 重点来了哈,咱们会发现创建的线程被设置为了非守护线程,即t.setDaemon(false),那么咱们重点看下setDaemon注释的这句话:
Marks this thread as either a {@linkplain #isDaemon daemon} thread
or a user thread. The Java Virtual Machine exits when the only
threads running are all daemon threads.
- 简单翻译下上面这句话,就是标记一个线程为一个守护线程或用户线程,JVM只有当运行的线程都是守护线程时才会退出,然而咱们线程池创建的都是非守护线程,并且我们根据源码,在Worker启动后,会调用到getTask(),这个方法中只要allowCoreThreadTimeOut没有被设置为true,Worker都会调用阻塞队列(当前使用ArrayBlockingQueue)的take方法,咱们看下该方法:
/**
* Retrieves and removes the head of this queue, waiting if necessary
* until an element becomes available.
*
* @return the head of this queue
* @throws InterruptedException if interrupted while waiting
*/
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
- 会发现阻塞队列中没有数据时,会一直阻塞,所以咱们的线程池创建的Worker对象一直都在运行,只不过是被阻塞了,所以咱们启动的main方法一直没有结束(哈哈哈,虽然说本文不讲线程池运行原理,这里其实也讲了,就是线程池创建线程后会源源不断的从阻塞队列中获取任务,这也就是线程池为啥能够将线程复用起来,因为创建了线程根本没销毁,其run方法不是在执行任务,就是在获取任务的路上)。
四种拒绝策略都适用于什么场景
- 因为技术永远没有最好,只有最适合,所以基于上述咱们对拒绝策略的分析,咱们大致可以将拒绝策略分别使用到如下场景,
- CallerRunsPolicy经分析触发时会阻塞主线程,所以可以应用在那种流程很重要,但是需要需要多线程处理的场景。
- DiscardOldestPolicy我们就可以用在报表导出,像咱们导出时不想阻塞线程,避免前端等待过久,咱们都会采用异步的方式(文件导出后写入到表中,业务可以后续在页面上查看)去导出对吧,那么就可以采用该策略,因为就算丢弃了咱们可以重新触发嘛。
- AbortPolicy(线程池默认的拒绝策略)一触发就报错,一般应用于一些对数据处理非常敏感,例如对一些数据落盘时,数据插入可能有多个逻辑处理,并且还要存储多个地方,例如mysql、redis、es,而且这些如果落盘失败可能会导致业务后续处理失败,那么咱们就可以采用该策略,资源不足时立即报错,阻止一些脏数据落库,避免影响整体业务流程,问题早发现早解决。
- DiscardPolicy可以应用于告警,因为就算是告警信息丢了也无伤大雅,后续实在有问题,依然会持续推送告警。
最后,因为之前一直不太了解,线程池拒绝策略到底是如何工作的,这几天重点看了源码实现,设计确实精妙,因为线程池涉及东西太多,只能拿出拒绝策略这一部分来讲,后续如果有时间,咱们再一起完整的讨论下线程池原理。如果各位同学觉得对你有所帮助,请关注、点赞、评论、收藏来支持我,手头宽裕的话也可以赞赏来表达各位的认可,各位同学的支持是对我最大的鼓励。未来为大家带来更好的创作。
分享一句非常喜欢的话:把根牢牢扎深,再等春风一来,便会春暖花开。
版权声明:以上引用信息以及图片均来自网络公开信息,如有侵权,请留言或联系
504401503@qq.com,立马删除。