当FutureTask遇上DiscardPolicy,有坑
创始人
2024-05-10 08:41:19
0

文章目录

    • 有啥坑呢?
    • 知识回顾
    • 问题触发条件
    • 问题复现
    • 问题分析
    • 问题修复
    • 扩展

哈喽,你好,我是余数。今天来了解下当 FutureTask 遇上 DiscardPolicyDiscardOldestPolicy 时容易掉的坑,然后分析分析问题产生的原因以及如何规避这类问题。

有啥坑呢?

获取异步执行结果时可能会导致主线程阻塞。
假如我向线程池中提交了5个带返回值的任务,编号为从0到4,但是我可能只能够获取到任务0和任务1的结果,获取任务2时主线程阻塞。

知识回顾

分析问题之前,我们先回顾一下JDK线程池自带的4个拒绝策略,它们分别是:

  1. CallerRunsPolicy:使用调用方线程中执行被拒绝的任务。
  2. AbortPolicy:拒绝任务,并抛出RejectedExecutionException异常。ThreadPoolExecutorScheduledThreadPoolExecutor的默认处理程序。或者任意其他自定义的,直接抛弃任务,但不抛出异常的策略。
  3. DiscardPolicy:直接丢弃被拒绝的任务,不抛出任何异常。
  4. DiscardOldestPolicy:丢弃队列中最老的任务,不抛出任何异常。

关于FutureTask,我们主要用它来获取异步线程的执行结果。在异步多线程的使用中又必定离不开线程池,使用线程池就一定会用到线程池的拒绝策略,因为不管多大的池子,它总是会满的不是吗?

问题触发条件

什么时候会出现这个问题呢?

  1. 向线程池提交(submit)任务,返回 Future。或者让线程池直接执行(execute)带返回值的任务FutureTask
  2. 线程池满,触发拒绝策略。且拒绝策略是 DiscardPolicy 或者 DiscardOldestPolicy
  3. 在发起异步的主线程调用 FutureTaskget()方法获取异步执行结果。

问题复现

为了方便,我使用了Spring的ThreadPoolTaskExecitor,其底层实现还是JDK的ThreadPoolExecutor。核心线程数、最大线程数和队列容量全部设置为1,主要是为了快速触发线程池的拒绝策略。

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;public class TestFutureTaskAndDiscardPolicy {public static void main(String[] args) throws ExecutionException, InterruptedException {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(1);executor.setMaxPoolSize(1);executor.setQueueCapacity(1);executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());executor.initialize();// FutureTask futureTask = new FutureTask(new MyCallable("任务-" + i));// executor.execute(futureTask);List res = new ArrayList<>();for (int i = 0; i < 5; i++) {Future futureTask = executor.submit(new MyCallable("任务-" + i));res.add(futureTask);}for (int i = 0; i < 5; i++) {Future futureTask = res.get(i);System.out.println(futureTask.get());}}static class MyCallable implements Callable {private String name;public MyCallable(String name) {this.name = name;}@Overridepublic String call() throws Exception {System.out.println(name + ":开始。。。");try {Thread.sleep(1_000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(name + ":结束。。。");return name + ":成功返回";}}
}

当FutureTask遇上DiscardPolicy,有坑

问题分析

首先可以定位到问题是出现在 获取异步任务结果 这里,那么为什么这里会出现阻塞呢?
当FutureTask遇上DiscardPolicy,有坑

答案就在FutureTask的源码里,查看源码会发现,当FutureTask的状态(state)小于等于 COMPLETING 时,线程会等待,直到这个任务完成或者等待超时。因为默认等待时间是0,也就是不会超时,线程会一直等待下去,也就是我们文章开始提到的坑,主线程卡死了。
当FutureTask遇上DiscardPolicy,有坑
当FutureTask遇上DiscardPolicy,有坑

现在我们知道了阻塞的原因是因为 FutureTask 的状态不对,那么 FutureTask 的状态又是为啥不对呢?
我们继续从源码找起,当我们向线程池提交一个带返回值任务的时候,线程池会将任务封装成一个FutureTask对象,而FutureTask对象默认新建时的状态为NEW
当FutureTask遇上DiscardPolicy,有坑
当FutureTask遇上DiscardPolicy,有坑
当FutureTask遇上DiscardPolicy,有坑

看到这里,一切正常,看不出什么问题,那么我们接着往下走。我们根据BUG触发条件可以知道,当线程池满触发拒绝策略时会出现问题,那么我们就去看看对应的源码。

以下为 ThreadPoolExecutor.execute(Runnable command) 的源码,红框圈起来的就是执行拒绝策略。
当FutureTask遇上DiscardPolicy,有坑

点进去可以看到,ThreadPoolExecutor 使用其 handler 执行拒绝策略。
当FutureTask遇上DiscardPolicy,有坑

当我们将handler设置成DiscardPolicy的时候,其rejectedExecution方法什么都没有执行,就是个空方法。
当FutureTask遇上DiscardPolicy,有坑

至此,从提交任务到拒绝任务再到获取任务结果,整个流程我们都走了一遍,我们发现了什么?是不是从开始到结束,FutureTask的状态都没有改变呢?一直都是新建时赋值的NEW状态,在这个状态下去获取执行结果是获取不到的,因为它还没有执行,所以就会一直等待它去执行,但是它其实已经被线程池拒绝掉了永远也不会执行了,so,卡死了。

这里又有个疑问了,那为什么默认拒绝策略AbortPolicy没有问题呢?答案是AbortPolicy拒绝时抛出了异常。这个异常会直接抛到主线程,所以需要在主线程捕获并处理这个异常,因为已经知道异常了,所以不会再去获取异步执行结果也就不会卡死了。
当FutureTask遇上DiscardPolicy,有坑
当FutureTask遇上DiscardPolicy,有坑

问题修复

既然知道导致问题的原因是线程池拒绝策略 DiscardPolicyDiscardOldestPolicy 导致的,那么就不用这两个策略嘛,如果一定要用的话,可以自己改造下,抛个异常出来可好?让主线程知道这个任务被拒绝了好进行下一步的操作,放弃也好,补偿也罢。

另外在获取异步结果的时候,可以加个超时时间保底,不至于让主线程阻塞。
当FutureTask遇上DiscardPolicy,有坑
修改后的结果,任务0和任务1成功执行,任务2、任务3、任务4被拒绝。
当FutureTask遇上DiscardPolicy,有坑

扩展

FutureTask 的状态流转是什么样的呢?
答案依然在源码里,在FutureTaskrun()方法中。
当FutureTask遇上DiscardPolicy,有坑

执行成功,最终状态为 NORMAL
当FutureTask遇上DiscardPolicy,有坑

出现异常时,最终状态为 EXCEPTIONAL
当FutureTask遇上DiscardPolicy,有坑

另外还可以通过cancel方法将状态改为 CANCELLED 或者 INTERRUPTEDINTERRUPTINGINTERRUPTED 的过渡状态。

只有状态为 NORMAL 时可以获取到执行结果,CANCELLEDINTERRUPTED 状态抛出CancellationException 异常,状态为 EXCEPTIONAL 时抛出 ExecutionException 异常。
当FutureTask遇上DiscardPolicy,有坑

相关内容

热门资讯

星火成炬|打开世界的方式   同样的夜晚灯火,有人看到璀璨街景,有人感怀思念故乡;同样的树叶飘落,有人伤感时间流逝,有人想到生...
权威数读丨金融支持实体经济:效...   2025年中国金融总量较快增长、社会综合融资成本进一步降低、金融结构不断优化、金融市场平稳运行,...
视频丨日本民众集会反对政府推进...   14日,部分日本民众在首相官邸前举行集会,要求日本首相高市早苗立即撤回错误言论,强烈抗议日本政府...
【中国那些事儿】外资持续加码彰...   中国日报网1月15日电 新年伊始,外资企业纷纷加码中国市场,多家企业宣布增加投资或启动新设施规划...
新华社评论员:切实把党中央重大...   新华社北京1月14日电 题:切实把党中央重大决策部署贯彻落实好——二论学习贯彻习近平总书记在二十...
“十五五”开好局起好步丨冰面上...   今年是“十五五”开局之年,我国将出台智能网联新能源汽车产业发展规划,以此推动新能源汽车产业高质量...
“十五五”电网投资4万亿元,创...   国家电网公司全面贯彻习近平新时代中国特色社会主义思想,深入贯彻党的二十大和二十届历次全会精神,坚...
著名电影表演艺术家陶玉玲逝世 ...   总台记者获悉,著名电影表演艺术家陶玉玲于今天(1月15日)下午在北京去世,享年92岁。其代表作有...
人民论坛网评 | 县域经济向“...   中央经济工作会议提出,统筹推进以县城为重要载体的城镇化建设和乡村全面振兴,推动县域经济高质量发展...
保险不只是合同,更是承诺 人们对保险的误解,往往从“花钱买纸”开始。很多人以为,交保费就是为了换一份合同,要是没出险,这笔钱就...