当FutureTask遇上DiscardPolicy,有坑
创始人
2024-05-10 06:05:10
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,有坑

相关内容

热门资讯

【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
【前端】‘??‘与‘||‘有什... 0 问题 经常写const data = res.data.a ?? ''或者const d...
ChatGPT 怎么用最新详细... ChatGPT 以其强大的信息整合和对话能力惊艳了全球,在自然语言处理上面表现出了惊人...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...