FutureTask为何单个任务仅执行一次原理解析

 更新时间:2023年11月15日 09:42:21   作者:zhongh Jim  
这篇文章主要为大家介绍了FutureTask为何单个任务仅执行一次原理解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

引言

前几天会员领取情况查询的接口SQL查询超时出故障了,因为有个用户买的会员有点多(哈哈),其实是 数据量大 + 祖传代码逻辑冗长

尝试的解决方案:

SQL:检查了一下,单个SQL的耗时其实不算大,也能接受,不需要改动,主要原因是后端逻辑冗长

FutureTask获取线程的执行结果:将1次大查询划分为多次小查询同时进行,提高接口响应速度。且一个FutureTask仅执行一次,不会出现重复的查询

经过权衡,我们选择了后者

一、FutureTask用法

解决方案要用到线程池搭配FutureTask,这里我们就不用了,简化点

public class Test {
    //计算结果
    int count=0;
    @Test
    public void test(){
        try{  
            FutureTask<Integer> futureTask=new FutureTask<>(new Callable<Integer>() {
                @Override
                public Integer call() throws Exception {
                    return 1;
                }
            });
            //把FutureTask放入线程中,线程会运行FutureTask的run()代码块
            Thread t1=new Thread(futureTask);
            t1.start();
            //获取计算的结果,是一个阻塞等待返回的方法
            count+=futureTask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        //最后结果: 1
        System.out.println(count);    
    }
}

这里用了构造方法public FutureTask(Callable<V> callable)让FutureTask持有Callable接口的实例

用到try-catch是由于futureTask.get()方法是一个阻塞等待的过程,途中如果被中断会抛中断异常,别的异常都会以ExecutionException执行异常的形式抛出

二、(重要)FutureTask的任务仅执行一次,为何?

FutureTask的run()代码块仅执行一次!请看注释

/**
    执行结果(全局变量), 有2种情况:
    1. 顺利完成返回的结果
    2. 执行run()代码块过程中抛出的异常
*/
private Object outcome; 
//正在执行run()的线程, 内存可被其他线程可见
private volatile Thread runner;
​​​​​​​public void run() {
    /**
        FutureTask的run()仅执行一次的原因:
        1. state != NEW表示任务正在被执行或已经完成, 直接return
        2. 若state==NEW, 则尝试CAS将当前线程 设置为执行run()的线程,如果失败,说明已经有其他线程 先行一步执行了run(),则当前线程return退出
    */
    if (state != NEW ||!UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()))
        return;
    try {
        //持有Callable的实例,后续会执行该实例的call()方法
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            }catch (Throwable ex) {
                result = null;
                ran = false;
                //执行中抛的异常会放入outcome中保存
                setException(ex);
            }
            if (ran)
                //若无异常, 顺利完成的执行结果会放入outcome保存
                set(result);
        }
    }finally {
        // help GC 
        runner = null;
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

执行run()的代码块之后,其他线程如何拿到FutureTask的执行结果?下面的get()方法可以做到

三、get()获取结果

public V get() throws InterruptedException, ExecutionException {    
       int s = state;
       //COMPLETING: 正在完成的状态;  s <= COMPLETING就是未完成
    if (s <= COMPLETING)
        //不计时等待,结束等待的条件只有【完成】、【被中断】、【被取消】、【抛其他异常(不包括中断异常、取消异常)】
        s = awaitDone(false, 0L);
    return report(s);    
}

这里提一下线程执行的状态 :

private volatile int state;
//线程创建状态
private static final int NEW = 0;
//完成(**一个瞬时的标记**)   
private static final int COMPLETING = 1;
//正常完成状态
private static final int NORMAL = 2;
//执行过程出现异常
private static final int EXCEPTIONAL = 3;
//执行过程中被取消
private static final int CANCELLED = 4;
//线程执行被中断(**一个瞬时的标记**)
private static final int INTERRUPTING = 5;
//线程执行被中断的状态
private static final int INTERRUPTED = 6;

volatile保证了线程执行的状态改变之后会刷新到内存中,被其他线程可见

如果线程还处于未完成的状态,即s <= COMPLETING,就会进入等待状态,调用awaitDone(false, 0L)方法

get为何阻塞等待?

/**
    @param timed 若是true则为定时等待,超时后会结束等待,并返回当前状态state
    @param nanos 如果是定时等待即第一个入参timed=true的话,会设置对应的等待时长
*/
private int awaitDone(boolean timed, long nanos) throws InterruptedException {
    //等待的最后期限
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    //进入无限循环的等待状态,只有【完成】、【被取消】、【异常】、【中断】、【超时】这五种情况才会结束等待
    for (;;) {
        if (Thread.interrupted()) {
            //线程执行被中断,则移除等待结点并抛出异常
            removeWaiter(q);
            throw new InterruptedException();
        }
         int s = state;
        //【完成】、【被取消】、【抛其他异常】的状态都会 在这 结束等待
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        //子线程处于任务完成的瞬时状态,要等一会才能拿到执行结果
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        else if (q == null)
            q = new WaitNode();
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q);
        else if (timed) {
            //设置定时等待并且已经超时了
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        else
            LockSupport.park(this);       
    }
}

详细的注释在代码中,请耐心看一下。

简单来说,能结束等待的条件只有5个:

  • 完成
  • 被中断
  • 设置定时等待并超时
  • 被取消
  • 抛了其他异常,比如RuntimeException,这里的其他异常既不是中断异常,也不是取消异常

调用futureTask.get()的等待方式有2种,分为定时等待和 不计时等待:

  • timed=true是定时等待,会创建等待结点q = new WaitNode();并放在栈顶(队列头部),然后挂起。结束等待的条件(满足任一即可)是【完成】、【被中断】、【被取消】、【抛其他异常】、【超时】 。
  • timed=false是不计时等待,创建等待结点后会一直挂起,只有【完成】、【被中断】、【被取消】、【抛其他异常】

在等待结束之前,LockSupport.park(this);表示线程会被一直挂起,不再继续无限循环占用CPU。

解除挂起的条件是state > COMPLETING,然后调用finishCompletion()方法去让线程解除挂起并回到awaitDone()做最后一次循环后return state

从get中返回结果report(int s)

/*正常的计算结果 or 抛出的异常 都会作为outcome*/
private Object outcome;
private V report(int s) throws ExecutionException {
    Object x = outcome;
    //正常完成
    if (s == NORMAL)
        return (V)x;
    //执行的过程中【被取消】
    if (s >= CANCELLED)
        throw new CancellationException();
    /**
        这里抛的是执行过程中发生的其他异常,既不是【中断异常】,也不是【被取消异常】
        比如发生了RuntimeException之类的就会在这抛
    */    
    throw new ExecutionException((Throwable)x);
}

report(int s)是执行get()获取结果的最后一步

看到这可能有朋友晕了,我把get()内部的流程梳理一下:

若要等待计算结果:get() -> awaitDone() -> report(),共3步
不用等待:get() -> report() ,仅2步

四、FutureTask是如何拿到线程执行的结果?

主要 有赖于FutureTask类内部的Callable接口

只有Callable接口能拿到线程的返回值,下面来看下FutureTask的构造函数

public class FutureTask<V> implements RunnableFuture<V> {
    //执行任务并返回结果
    private Callable<V> callable;
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        //新建状态
        this.state = NEW;
    }
}

其实Callable 接口是没法 作为创建线程new Thread(Runnable target)的入参的,只有借助FutureTask类才能被线程执行,因为FutureTask实现了Runnable 接口

有兴趣的可以看一下Future接口的关系图(这里拿了大佬的图,侵删)

FutureTask类最终实现了Future接口和Runnable接口,可作为new Thread(Runnable target)的入参target来创建线程

五、FutureTask可能的执行过程

顺利完成 :NEW -> COMPLETING -> NORMAL ,即新建->正在完成 ->正常

NEW -> COMPLETING -> EXCEPTIONAL, 执行过程出现了异常

被取消:NEW -> CANCELLED

NEW -> INTERRUPTING -> INTERRUPTED,新建 ->正在被中断 ->中断完成

六、列举一下FutureTask的特性和应用场景

特性:

  • 异步执行,可执行多次(通过runAndReset()方法),也可仅执行一次(执行run()即可)
  • 可获取线程执行结果

应用场景:

  • 长时间运行的任务,包含远程调用的任务
  • 数据量大的查询,划分为多个小查询,每个FutureTask 仅执行一次 的特性能有效避免重复的查询
  • 计算密集型的任务

以上就是FutureTask为何单个任务仅执行一次原理解析的详细内容,更多关于FutureTask单任务执行一次的资料请关注脚本之家其它相关文章!

相关文章

  • 关于Spring Boot动态权限变更问题的实现方案

    关于Spring Boot动态权限变更问题的实现方案

    这篇文章主要介绍了Spring Boot动态权限变更实现的整体方案使用session作为缓存,结合AOP技术进行token认证和权限控制,本文给大家介绍的非常详细,需要的朋友参考下吧
    2021-06-06
  • Spring整合MyBatis图示过程解析

    Spring整合MyBatis图示过程解析

    这篇文章主要介绍了Spring整合MyBatis图示过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-11-11
  • springBoot 整合ModBus TCP的详细过程

    springBoot 整合ModBus TCP的详细过程

    ModBus是一种串行通信协议,用于从仪器和控制设备传输信号到主控制器或数据采集系统,它分为主站和从站,主站获取和编写数据,从站则是设备,本文给大家介绍springBoot 整合ModBus TCP的详细过程,感兴趣的朋友一起看看吧
    2025-01-01
  • 分析讲解Java Random类里的种子问题

    分析讲解Java Random类里的种子问题

    Random类中实现的随机算法是伪随机,也就是有规则的随机。在进行随机时,随机算法的起源数字称为种子数(seed),在种子数的基础上进行一定的变换,从而产生需要的随机数字
    2022-05-05
  • idea创建SpringBoot项目及注解配置相关应用小结

    idea创建SpringBoot项目及注解配置相关应用小结

    Spring Boot是Spring社区发布的一个开源项目,旨在帮助开发者快速并且更简单的构建项目,Spring Boot框架,其功能非常简单,便是帮助我们实现自动配置,本文给大家介绍idea创建SpringBoot项目及注解配置相关应用,感兴趣的朋友跟随小编一起看看吧
    2023-11-11
  • 深入理解Java中的final关键字_动力节点Java学院整理

    深入理解Java中的final关键字_动力节点Java学院整理

    Java中的final关键字非常重要,它可以应用于类、方法以及变量。这篇文章中我将带你看看什么是final关键字以及使用final的好处,具体内容详情通过本文学习吧
    2017-04-04
  • 一篇文章带你了解mybatis的动态SQL

    一篇文章带你了解mybatis的动态SQL

    这篇文章主要为大家介绍了mybatis的动态SQL ,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-01-01
  • Java内存模型JMM与volatile

    Java内存模型JMM与volatile

    这篇文章主要介绍了Java内存模型JMM与volatile,Java内存模型是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,定义了程序中各个变量的访问方式
    2022-07-07
  • 如何对quartz定时任务设置结束时间

    如何对quartz定时任务设置结束时间

    这篇文章主要介绍了如何对quartz定时任务设置结束时间问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • Java 常量池详解之class文件常量池 和class运行时常量池

    Java 常量池详解之class文件常量池 和class运行时常量池

    这篇文章主要介绍了Java 常量池详解之class文件常量池 和class运行时常量池,常量池主要存放两大类常量:字面量,符号引用,本文结合示例代码对java class常量池相关知识介绍的非常详细,需要的朋友可以参考下
    2022-12-12

最新评论