Java Future

Outline

  1. 什么是Java Future
  2. 为什么要使用Java Future (好处?)
  3. 如何使用Java Future

一、什么是Java Future

1. 古老的Runnable

长久以来,程序员熟知的Java多线程是以Thread和Runnable为代表的,不必关心执行结果的异步操作。这也是JDK从一开始(JDK1.0)就支持的异步模式,这种异步操作的使用时序一般如下:

Java Future_第1张图片
runnable时序

这种模式中,我们不关心耗时操作的结果是什么,甚至不关心耗时操作要执行多久;可能在返回客户端后耗时操作都没有执行完毕。那么客户端请求执行的时间,近乎完全是工作线程”其他操作“的耗时。

伪代码如下:

Object foo() {
    // logic operation
    new Thread().start(() - {
        try {
            // time-consuming operation.
        } catch (PossibleException e) {
            // deal exception in the thread;
        }
    });
    // other logic operation
    return value;
}

这种不关心结果的异步操作设计,约定了run 方法不能抛出检查异常,而且线程必须自己处理执行过程中的异常。其实这也是个合情合理的约定,假设现在现在允许抛出异常到线程外部,试问在这种不关心结果的"shoot but don't care"的处理方式下,哪个时机处理线程抛出的异常合适呢?答案是只能线程自己内部处理。

2. 现在要关心结果了

如果要关心异步操作的结果,该怎么办呢? 瞬间涌上脑门的想法是,工作线程和耗时任务线程共享变量,工作线程通过轮询不断检查共享变量的方式来判断任务是否完成。结果就是以下的复杂时序:

Java Future_第2张图片
关心结果时序

注意工作线程检查结果与耗时任务线程设置结果并无先后关系

可能的伪代码如下:

Object sharedVar;
Object bar() {
    // logic operation
    new Thread(() -> {
        try {
            // time-consuming operation.
            sharedVar = value;
        } catch (PossibleException e) {
            // deal exception in the thread;
        }  
    }).start(); 
    // other logic operation
    while(!sharedVar ready);
    return sharedVar;
}

当然伪代码并没有考虑同步操作。

这样客户端请求的耗时就是工作线程的执行时长和耗时线程任务时长中较长的那个。现在想要更多的功能,比如:

  1. 因为一些情况,工作线程在耗时任务线程没有执行完的情况下不关心结果了,想要取消;
  2. 工作线程不想一直轮询来查询结果是否准备好,想要一个超时方法来指定轮询的时间。

当然这些合理的需求都是可以实现的,因此,从JDK 5开始Java为程序员准备了 Future 特性。

3. Java Future

正如以上,java future主要为异步操作提供了获取结果的途径。然后我们上面想要的,它都有。有了JDK的future特性,以上时序可以简化为:

Java Future_第3张图片
future时序

JKD提供java.util.concurrent.Future 来支持future特性, JDK对其的介绍是:

Future 表示异步计算的结果。 并提供检查异步计算是否完成的方法,等待完成的方法,以及检索计算结果的方法。结果只能使用get 在计算完成之后获取,如果计算没有完成,get的调用将会阻塞。调用cancle 方法可以取消异步计算。也提供额外的方法来检查任务是否正常完成,或是被取消了。一旦计算完成,就不可取消了。如果想要使用Future 异步任务可取消的特性,而不关心返回值,可以声明为Futre 类型,并在异步方法中返回null

二、为什么要使用Java Future

在我最开始几年的业务编程历史中,从未使用过JDK Future,所有的异步操作场景都是使用不关心结果的Thread+Runnable,因为似乎完全没有使用Future特性的需求。

我的想法是这样的: 即使用了Future,那我也是想要获取结果才会用的,之后还得调用阻塞方法get 来获取结果,可能的编码是这样的:

void bar() {
    ExecutorService service = Executors.newFixedThreadPool(1);
    Future future = service.submit(() -> {
            // time-consuming operation
            return value;
    });
    // blocking invoke
    Object result = future.get();
    // use result to do something...
}

那么这样做跟直接同步调用耗时操作有什么区别:

var bar() {
    // time-consuming operation, get value;
   Object value = doTimeConsuming();
   // use result to do something...
}

诚然,本质上看来,如果是这种使用方式的话,这两种方法的耗时并没有差别,使用Future可能会更慢,还更复杂。
这个业务场景,其实是依赖耗时操作的结果,来做一些事情,这种场景下使用future意义不大,用单个线程完全可以胜任。

来杯咖啡提提神

但是啊,成年人的世界都是多线程的。

现实生活中,这样的场景随处可见。比如去咖啡馆泡吧看书,首先点一杯咖啡,咖啡制作过程中,我们也不可能在吧台闲着等咖啡做好;而是找到一个有插座的位置,摆好电脑,插上电源,启动电脑,打开idea,开始撸代码。做一个极端的假设,服务员不通知我们咖啡做好了,而是我们实在困得不行需要咖啡刺激一下的时候,自然会去吧台看看咖啡做好了没;没有做好就一直等(没办法,需要咖啡的刺激才能有精神继续撸代码啊),或者不是很困的话,等一会儿发现还没做好,那就再回去撸两行代码,再来吧台看看。

抽象一下这个业务场景,首先明确存在两个耗时操作:

  1. 制作咖啡
  2. 看书学习敲代码

操作2,在某一时刻(困得不行的时候)需要操作1的结果才能进行下去,但不是在一开始就需要依赖操作1的结果。在某一段时间两个操作需要并行,可能的时序如下:

Java Future_第4张图片
喝咖啡

那么总结一下future的使用场景:

  1. 多个在某个时间段可并行的耗时操作,并且存在任务结果依赖
  2. Future类的jdk说明中提到的,如果想使用future异步任务可取消的特性,也可以使用。

三、如何使用Java Future

虽然JDK提供了多种途径使用Java Future,但是它们本质相同,使用相当简单,主要提供两种方式:

  1. Callable + ExecutorService方式
  2. FutureTask + (ExecutorService/Thread)方式

1. Callable + ExecutorService

这种方法通过不同的方式创建Callable 实例, 然后依赖ExecutorServicesubmit 方法获得Future 返回,最后调用返回的Future 实例get 方法获取结果。

Callable 接口:

public interface Callable {
    /**
    *  计算结果,计算失败时抛出异常
    */
    V call() throws Exception;
}

JDK对于Callable的描述:

Callable是一个会返回结果或者抛出异常的任务。实现类定义一个无参的,名为call 的方法。与Runnable 接口类似,都是为可能在其他线程中执行的类实例设计的。但是Runnableb不返回结果,也不抛出异常。 Executors 类提供将其他一般形式转换为Callable类的工具方法。

其实jdk对Callable的描述清楚阐述了创建Callable实例的方法:

  1. 实现Callable接口

    class CallableTask implements Callable {
         @Override
         public Object call() throws Exception {
             // do something
             return null;
         }
     }
     CallableTask instance = new CallableTask();
      
      
  2. Executors工具方法

      // 这种方法的实现其实就是用一个Callable适配器
      java.util.concurrent.Executors#callable(java.lang.Runnable, T);
      
     Callable instance = Executors.callable(() -> {
         // runnable task;
     }, "return value");
    
  3. 创建好Callable实例之后,使用Executorssubmit(callable: Callbale) 即可:

    ExecutorService service = Executors.newFixedThreadPool(1);
    Future future = service.submit(callable);
    // blocking invoke
    Object result = future.get();
    

    2. FutureTask + (ExecutorService/Thread)

    来近距离观察一下FutureTaskFuture 类的关系:

    Future          Runnable
        \             /
          \          /
            \      /
          RunnableFuture
                |
                |
           FutureTask
    

    RunnableFuture 正如其名,是runnbale的future,意味着可以通过线程池的submit 方法,或者使用new Thread(runnable: Runnable) 方法异步地执行。成功执行RunnableFuturerun 方法,意味着future的完成,同样允许访问future的结果。

    RunnableFuture 本身已经是一个Future实例了,只需将其异步执行,即可成为一个有效的Future实例,可以调用get方法阻塞获取结果了。

    FutureTask 直接实现RunnableFuture 接口,提供两个构造方法创建FutureTask 实例:

    public class FutureTask implements RunnableFuture {
        public FutureTask(Callable callable) {
            this.callable = callable;
            ...
        }
        
        public FutureTask(Runnable runnable, V result) {
            // 使用Executors工具类将runnbale转换为callbale, 本质与以上构造方法相同
            this.callable = Executors.callable(runnable, result);
            ...
        }
    }
    

    所以明面上创建FutureTask 实例的构造方法:

    FutureTask task = new FutureTask<>(new Callable() {
        @Override
        public Object call() throws Exception {
            return null;
        }
    });
    
    FutureTask task = new FutureTask<>(new Runnable() {
        @Override
        public void run() {
        }
    }, "return value");
     
     

    接下来直接以异步的方式执行futureTask实例即可:

    ExecutorService service = Executors.newFixedThreadPool(1);
    // 1. java.util.concurrent.ExecutorService#submit(java.lang.Runnable), 不用关心返回的Future结果
    service.submit(futureTask);
    // blocking; 依然是使用futureTask的get方法,而不是submit返回的Future 实例
    Object retVal = futureTask.get();
    
    // 2. java.util.concurrent.Executor#execute(java.lang.Runnable),无返回值
    service.execute(futureTask);
    // blocking; 
    Object retVal = futureTask.get();
    
    // 3. new Thread(java.lang.Runnable).start();
    new Thread(futureTask).start();
    // blocking; 
    Object retVal = futureTask.get();
    

    可见futureTask完全不关心自己是被以何种方法异步执行,只是忠实于futureTask本身;

    捕获callable的异常

    callable与runnable的区别之一是callable可以抛出异常给其他线程处理,而不用在线程内部捕获处理消化异常。那么callable的异常是在哪里捕获呢?答案是在调用FutureTask.get()方法处:

    public static void main(String[] args) {
        ExecutorService service = Executors.newFixedThreadPool(1);
        Future future = service.submit(() -> {
            System.out.println("callable executing...");
            Thread.sleep(3_000L);
            throw new Exception("exception in callable");
        });
    
        try {
            future.get();
        } catch (InterruptedException e) {
            System.out.println("interrupt ex:" + e.getMessage());
        } catch (ExecutionException e) {
            System.out.println("execute ex:" + e.getMessage());
        } catch (Exception e) {
            System.out.println("general ex:" + e.getMessage());
        }
        service.shutdown();
    }
    

    output

    callable executing...
    execute ex:java.lang.Exception: exception in callable
    

    四、 Future的cancel

    前面讲述过,future的一个使用场景是:不为操作结果,只是用future可取消的特性。

    jdk关于java.util.concurrent.Future#cancel(boolean) 的描述是:

    尝试取消任务的执行。如果任务已经完成,已被取消,或者由于某些原因不能被取消,那么这次取消的尝试会返回失败。如果返回成功,并且取消方法调用时任务尚未开始,那么任务永远也不会执行。如果任务已经开始,那么参数mayInterruptIfRunning 决定了是否向线程发送中断标志,来尝试停止任务。
    cancle方法返回true后,接下来调用isDone 方法总是返回true,isCancelled 方法也会总是返回true。

    这里cancel方法有两种情况:

    1. 任务未开始,不管参数mayInterruptIfRunning 值是什么,如果返回成功,任务都不会开始;
    2. 任务已开始,mayInterruptIfRunning==true 时向任务线程发送中断状态,总是返回true;否则不发送中断状态,总是返回true;

    那么,实际上的cancel调用,不一定会导致future异步任务的终止。
    因为如果任务已经开始,只是向线程发送了中断状态:

    • 如果此时线程因为sleep, wait 等方法进入阻塞状态,那么会抛出InterruptedException 异常并结束;
    • 如果线程正常执行,只能在线程合适的地方,通过Thread.interrupted() 检查线程的中断状态,来决定任务是否结束;否则cancel的调用不会产生任何影响。

    以下代码简要说明了任务已经开始,如何通过cancel的调用取消future任务:

    public static void main(String[] args) {
        CountDownLatch c = new CountDownLatch(1);
    
        ExecutorService service = Executors.newFixedThreadPool(1);
        Runnable task = () -> {
            System.out.println("async task begin.");
            c.countDown();
            while (!Thread.interrupted()) ;
            System.out.println("thread is interrupted!!!");
            System.out.println("async task over.");
        };
        Future future = service.submit(task);
    
        // 保证异步任务已经开始
        try {
            c.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        System.out.println("cancel: " + future.cancel(true));
        service.shutdown();
    }
    

    但是这只是一个demo,实际代码中不可能只是不断检查中断状态而不做任务实际逻辑;所以,如果任务开始,取消真的很难呐。

    五、总结

    Java Future代表了异步任务的“未来结果”, 使用Future来与另一个线程交流,获取异步任务结果。

    Future的使用场景一般是:

    1. 业务包含多个可并行的耗时任务,并且存在结果依赖;
    2. 单纯想使用FutureTask可取消的特性的Runnable异步任务。

    使用Future的两种方法有两种:

    1. 一是通过Callable, Callable交给线程池执行,获取返回的Future实例;
    2. 构造一个FutureTask,FutureTask本身就是个Future实例,同样也是一个Runnable实例,异步执行它即可;

    你可能感兴趣的:(Java Future)