JAVA并发编程系列(13)Future、FutureTask异步小王子

本文涉及的产品
云原生网关 MSE Higress,422元/月
注册配置 MSE Nacos/ZooKeeper,118元/月
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 本文详细解析了Future及其相关类FutureTask的工作原理与应用场景。首先介绍了Future的基本概念和接口方法,强调其异步计算特性。接着通过FutureTask实现了一个模拟外卖订单处理的示例,展示了如何并发查询外卖信息并汇总结果。最后深入分析了FutureTask的源码,包括其内部状态转换机制及关键方法的实现原理。通过本文,读者可以全面理解Future在并发编程中的作用及其实现细节。

美团本地生活面试:模拟外卖订单处理,客户支付提交订单后,查询订单详情,后台需要查询店铺备餐进度、以及外卖员目前位置信息后再返回。

    时间好快,一转眼不到一个月时间,已经完成分享synchronized、volatile、CAS、AQS、ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier、并发锁、Condition、线程池、ThreadLocal等多个核心基础原理和案例剖析。

   其实我编写文章速度真的很慢,基本每篇文章需要写2~3个小时去梳理。确保基础理论、源码分析、面试案例、优缺点等均分享到位,力求每篇都是干货实用,让每位看到我文章的同学,不管是面试、还是应用到工作实践,都能有所收益。

   今天我们围绕Future是什么、怎么用,实践demo来展开分享,实现原理架构来展开。

一、Future是什么

     首先我们回到一个问题,就是为什么需要Future、FutureTask?

之前我们用过的线程池ThreadPoolExecutor、线程Thread都可以执行异步任务,但是无法执行带有返回值的异步任务。而Future是可以执行这种带有返回值的异步任务。线程池ThreadPoolExecutor、Thread线程可以通过提交执行Future类型的任务,就可以获取任务返回值。

     和Callable、Runnable一样,Future是一个接口。我们看一下它的接口源码。

//Runnable接口,只有一个run方法
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}


//Callable接口,只有一个call方法
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}


//本文主角Future有5个方法
public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

代码非常少,有5个方法,但是核心方法是get(),还是isDone();

get()方法:用来读取任务的返回结果。此外,如果任务未执行,调用该方法的当前线程会进入阻塞等待。

isDone()方法:检查计算是否已完成。这个方法不会阻塞线程。日常使用Future,一般是先调用isDone()方法判断结果是否返回,然后再调用get()方法获取执行结果。

一句话总结:Future是异步计算任务,提交Future任务后可以继续干别的;等干完别的事,再回来通过get()方法去读取Future任务结果

再简单总结:Future是支持在未来读取结果的异步计算任务。

cancel()方法:用来尝试取消任务,仅仅是尝试,不一定成功。如果任务已经开始执行,那么它就不能被取消。

isCancelled()方法:就是用来检查这个Future任务是否被取消。

      Future和之前分享的信号量Semaphore、CountDownLatch倒数门闩、CyclicBarrier循环屏障都不一样,唯一和Condition条件队列有点像,支持多个线程协调进行。支持异步读取任务结果这个特性,Future可以很方便支持多个任务并发执行,以及在最后汇总获取并发结果,最后返回给终端。


二、应用实践:模拟同时查外卖信息

我们用FutureTask实现一个并发面试题:模拟外卖订单处理,客户支付提交订单后,查询订单详情,后台需要查询店铺备餐进度、以及外卖员目前位置信息后再返回。


package lading.java.mutithread;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
 * 模拟外卖订单处理,客户支付提交订单后,查询订单详情,后台
 * 通过查询店铺备餐进度、以及外卖员目前位置信息。
 */
public class Demo015Future {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1、异步查询商家系统任务
        FutureTask<Boolean> checkFoodIsReadyTask = new FutureTask<>(() -> {
            System.out.println(Thread.currentThread().getName()+"转发请求到商家系统,查询餐厅当前订单备餐进度...");
            Thread.sleep(2000);
            boolean foodIsOk = true;
            System.out.println(Thread.currentThread().getName()+"商家接口返回是否备餐完成,结果是:" + foodIsOk);
            return foodIsOk;
        });
        //2、异步查询外卖员系统任务
        FutureTask<Boolean> checkCourierIsReadyTask = new FutureTask<>(() -> {
            System.out.println(Thread.currentThread().getName()+"转发请求到外卖员系统,查询外卖员是否已到店...");
            Thread.sleep(800);
            boolean courierIsOk = true;
            System.out.println(Thread.currentThread().getName()+"查询外卖员是否已到店,结果是:" + courierIsOk);
            return courierIsOk;
        });
        //并发查询商家、外卖员情况,大约2s
        new Thread(checkFoodIsReadyTask).start();
        new Thread(checkCourierIsReadyTask).start();
        Thread.sleep(100);
        //只是判断是否完成,不会阻塞
        if (!checkFoodIsReadyTask.isDone()) {
            System.out.println(Thread.currentThread().getName() + "查询店铺备餐进度未完成,继续等等...");
        }
        if (!checkCourierIsReadyTask.isDone()) {
            System.out.println(Thread.currentThread().getName() + "查询外卖员情况未完成,继续等等...");
        }
        //如果结果没返回会阻塞等待
        System.out.println(Thread.currentThread().getName() + "线程 成功查到商家备餐结果:" + checkFoodIsReadyTask.get());
        System.out.println(Thread.currentThread().getName() + "线程 成功查到外卖员结果:" + checkCourierIsReadyTask.get());
    }
}

运行结果:


三、硬核干活:FutureTask的实现原理源码分析

    我们从它的属性开始,然后讲实现的方法原理。

3.1 FutureTask的属性

    FutureTask 的源码也不多,属性就这5个。

线程状态state、执行任务的callable,任务执行的返回结果outCome,正在运行的线程runner,等待队列里的waiters节点。

3.1.1 当前任务线程状态state,以及状态枚举

这七种任务状态之间相互转换关系:

1、正常结束  

NEW -> COMPLETING -> NORMAL

2、异常结束

NEW -> COMPLETING -> EXCEPTIONAL

3、任务被取消

NEW -> CANCELLED  

4、任务出现中断

NEW -> INTERRUPTING -> INTERRUPTED


//当前任务线程的状态,以下几个枚举值都是state不同状态
private volatile int state;
 
//当前任务线程刚创建状态
private static final int NEW          = 0;
//当前任务线程即将完成,ing是一个即将完成状态
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;

3.2 其他属性意义


//Callable 来执行任务,Callable 有返回值的线程
private Callable<V> callable;
 
//Callable任务执行的返回结果
private Object outcome; 
 
//当前正在运行的线程
private volatile Thread runner;
 
//等待队列节点,WaitNode是FutureTask的内部类
private volatile WaitNode waiters;

waiters等待队列:这个是单向队列,里面是等待本任务执行结果的线程。比如ABCD四个线程,其中A线程执行了任务,B、C、D线程,都等A线程的返回结果,就需要在waiters等待队列里等着。


//等待队列源码
    static final class WaitNode {
        //被阻塞的线程,也就
        volatile Thread thread;
        //这里看出它就是个单向队列
        volatile WaitNode next;
        WaitNode() { thread = Thread.currentThread(); }
    }

3.3 run()方法原理

   run方法比较简单,不放源码,直接说,就是执行定义的callable任务,任务执行完成后,通过CAS去更新outCome返回值。


protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }

3.4 get()方法原理

  这个方法和run()一样重要,其他线程想要获取本任务结果,都是通过get方法读取。逻辑也是很简单。其中waitDone()方法,就是进入等待等列等结果。


public V get() throws InterruptedException, ExecutionException {
        int s = state;
        //1、如果任务没执行完成,当前想读取任务结果的线程就进入阻塞等待
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        //2、如果任务执行完成,就返回结果    
        return report(s);
    }

3.5 其他方法

//当前任务状态是否被取消,直接读状态值
    public boolean isCancelled() {
        return state >= CANCELLED;
    }
    //当前任务状态是否以及执行结束,直接读状态值
    public boolean isDone() {
        return state != NEW;
    }

本系列文章推荐:

1、JAVA并发编程系列(12)ThreadLocal就是这么简单|建议收藏

2、JAVA并发编程系列(11)线程池底层原理架构剖析

相关文章
|
3天前
|
监控 安全 Java
Java多线程编程的艺术与实践
【10月更文挑战第22天】 在现代软件开发中,多线程编程是一项不可或缺的技能。本文将深入探讨Java多线程编程的核心概念、常见问题以及最佳实践,帮助开发者掌握这一强大的工具。我们将从基础概念入手,逐步深入到高级主题,包括线程的创建与管理、同步机制、线程池的使用等。通过实际案例分析,本文旨在提供一种系统化的学习方法,使读者能够在实际项目中灵活运用多线程技术。
|
2天前
|
Java
Java中的多线程编程:从基础到实践
本文深入探讨Java多线程编程,首先介绍多线程的基本概念和重要性,接着详细讲解如何在Java中创建和管理线程,最后通过实例演示多线程的实际应用。文章旨在帮助读者理解多线程的核心原理,掌握基本的多线程操作,并能够在实际项目中灵活运用多线程技术。
|
2天前
|
Java 程序员 开发者
Java编程中的异常处理艺术
【10月更文挑战第24天】在Java的世界里,代码就像一场精心编排的舞蹈,每一个动作都要精准无误。但就像最完美的舞者也可能踩错一个步伐一样,我们的程序偶尔也会遇到意外——这就是所谓的异常。本文将带你走进Java的异常处理机制,从基本的try-catch语句到高级的异常链追踪,让你学会如何优雅地处理这些不请自来的“客人”。
|
2天前
|
设计模式 SQL 安全
Java编程中的单例模式深入解析
【10月更文挑战第24天】在软件工程中,单例模式是设计模式的一种,它确保一个类只有一个实例,并提供一个全局访问点。本文将探讨如何在Java中使用单例模式,并分析其优缺点以及适用场景。
6 0
|
2天前
|
存储 Java
在Java编程的世界里,标识符命名是一项基础且至关重要的技能
在Java编程的世界里,标识符命名是一项基础且至关重要的技能
7 0
【小家Java】Future与FutureTask的区别与联系
【小家Java】Future与FutureTask的区别与联系
|
3天前
|
监控 安全 Java
在 Java 中使用线程池监控以及动态调整线程池时需要注意什么?
【10月更文挑战第22天】在进行线程池的监控和动态调整时,要综合考虑多方面的因素,谨慎操作,以确保线程池能够高效、稳定地运行,满足业务的需求。
71 38
|
5天前
|
Java 调度
[Java]线程生命周期与线程通信
本文详细探讨了线程生命周期与线程通信。文章首先分析了线程的五个基本状态及其转换过程,结合JDK1.8版本的特点进行了深入讲解。接着,通过多个实例介绍了线程通信的几种实现方式,包括使用`volatile`关键字、`Object`类的`wait()`和`notify()`方法、`CountDownLatch`、`ReentrantLock`结合`Condition`以及`LockSupport`等工具。全文旨在帮助读者理解线程管理的核心概念和技术细节。
18 1
[Java]线程生命周期与线程通信
|
2天前
|
安全 Java
在 Java 中使用实现 Runnable 接口的方式创建线程
【10月更文挑战第22天】通过以上内容的介绍,相信你已经对在 Java 中如何使用实现 Runnable 接口的方式创建线程有了更深入的了解。在实际应用中,需要根据具体的需求和场景,合理选择线程创建方式,并注意线程安全、同步、通信等相关问题,以确保程序的正确性和稳定性。
|
3天前
|
Prometheus 监控 Cloud Native
JAVA线程池监控以及动态调整线程池
【10月更文挑战第22天】在 Java 中,线程池的监控和动态调整是非常重要的,它可以帮助我们更好地管理系统资源,提高应用的性能和稳定性。
22 4