并发设计模式(3):Future模式

62 阅读7分钟

什么是Future模式?

Future模式是多线程开发中一种经典的设计模式,其核心思想是异步调用。它通过将耗时操作主线程分离,让主线程不必等待结果返回即可继续执行其他任务,从而显著提升系统的响应速度资源利用率

同步调用 image.png 异步调用

image.png

实现了Future模式的客户端在拿到这个返回结果后,并不急于处理,⽽是调⽤了其他业务逻辑,充分利⽤了等待时间,这就是Future模式的核心所在。在完成了其他业务逻辑的处理后,再使⽤返回⽐较慢的Future数据。

Future模式的主要参与者包括

  1. Main:系统入口,调用Client发起请求,处理其他业务,最终通过Future获取真实结果
  2. Client:接收请求后,立即返回FutureData,同时启动独立线程异步构建RealData
  3. Data:结果返回接口,定义获取真实结果的方法(如getResult()
  4. FutureData:虚拟结果实现类,封装RealData的构建等待逻辑,是Future模式的核心代理
  5. RealData:真实结果实现类,构造过程耗时(如数据计算、IO操作),最终通过getResult()返回

Future模式的简单实现

核心接口Data

Data就是客户端希望获取的数据,在Future模式中,Data接口有两个重要的实现:

  1. RealData:最终获取的真实数据
  2. FutureData:用于提取RealData
public interface Data<T> {
    public T getResult();
}

实现类FutureData

FutureData实现了一个可以快速返回的RealData包装,它只是一个虚拟实现,因此很快就可以返回。当使用FutureDatagetResult方法的时候,如果实际的数据还没有准备好,就会被阻塞,等待RealData准备好并注入FutureData中才最终返回。

public class FutureData<T> implements  Data<T> {
    protected RealData<T> realData; //FutureData是RealData的包装
    protected boolean isReady;
    @Override
    public synchronized T getResult() {
        while(!isReady){
            try{
               this.wait(); //数据还没准备好,线程阻塞,直到被唤醒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        return realData.getResult();//返回真实数据
    }
    public synchronized void setRealData(RealData realData) {
        if (isReady){
            return ;
        }
        this.realData=realData;
        this.isReady=true;
        this.notifyAll(); //RealData已经被注入,唤醒所有的阻塞线程
    }
}

实现类RealData

RealData是真实的数据,是最终需要使用的数据模型,它的构造非常慢。

public class RealData<T> implements Data<T> {
    private T data;
    public RealData(T data) throws InterruptedException {
        for (int i=0;i<10;i++){//使用循环睡眠来代替耗时操作
            System.out.println("正在构造数据...");
            TimeUnit.SECONDS.sleep(1);
        }
        this.data=data;
    }
    @Override
    public T getResult() {
        return data;
    }
}

客户端程序

Client实现了获取FutureData并开启构造RealData的线程。接收请求之后,会直接返回一个FutureData

public class Client {
    public Data<String> request(){
        final FutureData futureData=new FutureData();
        new Thread(()->{
            RealData<String> realData=new RealData<>("hello,world");
            futureData.setRealData(realData);
        }).start();
        return futureData;
    }
}

主函数Main

    public static void main(String[] args) throws InterruptedException {
        Client client = new Client();
        Data<String> request = client.request();
        for(int i=0;i<5;i++){
            System.out.println("处理其他事情中...");
            TimeUnit.SECONDS.sleep(1);
        }
        System.out.println(request.getResult());
        System.out.println("整个程序已结束");
    }

以下是程序输出的结果:

image.png

JDK中的Future模式

image.png Future接口就类似之前的FutureData是用来立即返回的对象,通过get()可以获取到真实的数据。

RunnableFuture继承了RunnableFuture接口,其中run方法用于线程构建真实的数据。

FutureTaskRunnableFuture的一个具体实现类,其内部有一个内部类SyncSync最终会调用Callble接口,完成实际数据的组装。

Callable接口调用call方法来返回需要构造的实际数据。

Future接口

Future接口用于异步任务中解决三个问题:

  1. 异步任务的结果怎么拿?
new Thread(() -> {
    String result = search();
}).start();

这个 result 是子线程里的局部变量,主线程拿不到。所以需要一个“结果容器”。Future就是这个容器。

  1. 结果还没准备好怎么办? Future需要提供一种机制:
    • 如果结果没准备好,就阻塞等待
    • 如果结果准备好了,就立即返回
  2. 异步的任务怎么取消?

用户点了“取消搜索”,那么后台任务没必要继续跑。

Java中的Future<V>代表着一个异步计算任务未来可能产生的结果,可以将其类别比为麻辣烫的号码牌,虽然餐还在做,但是会给你一张号码牌,之后就可以:

future.isDone();       // 看餐做好没
future.get();          // 等餐并取餐
future.cancel(true);   // 尝试取消订单

下面给出一个Future接口的使用示例:

ExecutorService executor = Executors.newSingleThreadExecutor();

Callable<String> task = () -> {
    Thread.sleep(3000);
    return "hello future";
};

Future<String> future = executor.submit(task);

System.out.println("主线程继续执行其他任务");

String result = future.get();

System.out.println("异步任务结果:" + result);

executor.shutdown();

Future中的方法解析:

boolean cancel(boolean mayInterruptIfRunning);

尝试取消任务,mayInterruptIfRunning表示任务已经开始执行,是否允许中断执行任务的线程。 比如:

future.cancel(true);

表示如果任务还没开始,就不需要执行了,如果任务已经开始,就尝试通过interrupt中断。

cancel不是强制杀死线程,而是调用线程的interrupt,任务能不能停下来,取决于任务代码是否响应中断

boolean isCancelled();

判断任务是否被取消

boolean isDone();

判断任务是否结束,注意结束并不代表任务成功

V get()

等待任务完成,并获取结果:

  1. 任务没完成的时候会阻塞
  2. 任务完成后返回结果
  3. 任务异常时会抛异常

Future本身不表示任务逻辑,任务逻辑通常由 RunnableCallable 表示。

Runnable是没有返回值的:

Runnable task = () -> {
    System.out.println("执行任务");
};
Future<?> future = executor.submit(task);
future.get();

返回的null

Callable有返回值:

Callable<String> task = () -> {
    return "hello";
};
Future<String> future = executor.submit(task);
String result = future.get();

同时,Future还保证了可见性,异步任务中发生的操作happen-before另一个线程中 Future.get() 成功返回之后的操作。

Future的局限性

Future解决了异步任务结果的问题,但是它有明显的缺陷:

  1. get方法是阻塞式的,如果任务没完成,当前线程就卡住。
  2. 无法方便地链式编排,这也是后续引入CompletableFuture的原因
  3. 取消不一定成功,它只是尝试中断,而不是强制停止

FutureTask

FutureTask是一个可执行的异步任务以及一个未来结果的凭证。

FutureTask的构造方法

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

这个构造方法只做了两件事:将Callable对象保存到数据域,将状态置为New

public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

这个构造方法将Runnable对象和result合起来构造出一个Callable对象,并且将状态设置为New

来详细看看Executors.callable()方法

public static <T> Callable<T> callable(Runnable task, T result) {
    if (task == null)
        throw new NullPointerException();
    return new RunnableAdapter<T>(task, result);
}

它实际返回一个RunnableAdapter对象。

private static final class RunnableAdapter<T> implements Callable<T> {
    private final Runnable task;
    private final T result;
    RunnableAdapter(Runnable task, T result) {
        this.task = task;
        this.result = result;
    }
    public T call() {
        task.run();
        return result;
    }
    public String toString() {
        return super.toString() + "[Wrapped task = " + task + "]";
    }
}

这是一个经典的适配器模式的实现,用于将Runnable适配成Callabletask是执行的真正任务逻辑,result预定义的返回结果。

// 只是执行Runnable,返回一个占位结果
Runnable task = () -> { System.out.println("Task done"); };
Callable<String> callable = new RunnableAdapter<>(task, "DONE");
// 执行后总是返回固定的"DONE",不关心对象状态变化

FutureTask的run

因为FutureTask本身是一个Runnable,因此它重写了run方法。

public void run() {
    if (state != NEW ||
        !RUNNER.compareAndSet(this, null, Thread.currentThread()))
        return;
    try {
        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;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

这段代码,首先会检查状态并确保唯一的线程执行,检查通过后,运行Callable对象执行实际的业务。最后将执行的结果通过cas设置到数据域的outcome中。

FutureTask的get

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}

如果当前的状态不为COMPLETING,则意味着数据还未准备好,于是陷入阻塞。直到数据准备好被唤醒。

否则直接调用report

private V report(int s) throws ExecutionException {
    Object x = outcome;
    if (s == NORMAL)
        return (V)x;
    if (s >= CANCELLED)
        throw new CancellationException();
    throw new ExecutionException((Throwable)x);
}

如果状态是NORMAL,那么返回结果。

如果被取消,返回取消异常。

如果执行中出错,返回执行中的错误。

FutureTask的cancel


public boolean cancel(boolean mayInterruptIfRunning) {
    if (!(state == NEW && STATE.compareAndSet
          (this, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {    // in case call to interrupt throws exception
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null)
                    t.interrupt();
            } finally { // final state
                STATE.setRelease(this, INTERRUPTED);
            }
        }
    } finally {
        finishCompletion();
    }
    return true;
}

如果任务尚未启动,取消后它不应该再运行;如果任务已经启动,mayInterruptIfRunning 决定是否尝试中断执行任务的线程。