Java Future

Outline

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

一、什么是Java Future

1. 古老的Runnable

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

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. 现在要关心结果了

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

关心结果时序

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

可能的伪代码如下:

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特性,以上时序可以简化为:

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的结果。在某一段时间两个操作需要并行,可能的时序如下:

喝咖啡

那么总结一下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> {
    /**
    *  计算结果,计算失败时抛出异常
    */
    V call() throws Exception;
}

JDK对于Callable的描述:

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

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

  1. 实现Callable接口

    class CallableTask implements Callable<Object> {
         @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<String> instance = Executors.callable(() -> {
         // runnable task;
     }, "return value");
    

创建好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<V> implements RunnableFuture<V> {
    public FutureTask(Callable<V> callable) {
        this.callable = callable;
        ...
    }
    
    public FutureTask(Runnable runnable, V result) {
        // 使用Executors工具类将runnbale转换为callbale, 本质与以上构造方法相同
        this.callable = Executors.callable(runnable, result);
        ...
    }
}

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

FutureTask<Object> task = new FutureTask<>(new Callable<Object>() {
    @Override
    public Object call() throws Exception {
        return null;
    }
});

FutureTask<String> 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实例,异步执行它即可;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容