FutureTask

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

为什么要介绍FutureTask呢?原因有两点:

  • Java中但凡和“线程池”、“异步线程”、“异步结果”有关的API,很大概率会出现FutureTask,是个很重要的类
  • FutureTask本身很有意思

FutureTask为什么有意思呢?

假设一个场景,要求设计一个方法,专门用来执行耗时操作。但不同点在于:

  • 调用时必须能够立即返回result
  • 拿到result后,我可以在随后任意时刻通过result.get()获取耗时任务的最终结果(异步结果)

这个需求的诡异之处在于:刚开始立即返回的result肯定不包含结果(毕竟耗时任务),但随后通过result.get()却又能得到结果!如果让我们从零开始实现这个需求,是比较困难的,好在JDK已经设计出FutureTask,它专门为这种场景而生。FutureTask就像个魔术盒,刚开始是空的,你抱着它走了几步,再打开时里面竟然出现了一个苹果,amazing!

希望上面的介绍能勾起大家对FutureTask的兴趣,然后带着好奇心阅读本文。

FutureTask:Thread大佬别生气,这两货我包了

大部分人既不认识Future也不知道Task,合在一起更是不知所云。不急,先来看看FutureTask是怎么用的。

初识FutureTask

public class AsyncAndWaitTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 第一次看到FutureTask时,相信大家会震惊:啥玩意,怎么把Callable往FutureTask里塞呢?!
        FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                System.out.println(Thread.currentThread().getName() + "========>正在执行");
                try {
                    Thread.sleep(3 * 1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return "success";
            }
        });
        

        // 看到这,你再次震惊:啥玩意,怎么又把FutureTask塞到Thread里了呢?!
        new Thread(futureTask).start();
        System.out.println(Thread.currentThread().getName() + "========>启动任务");


        // 可以获取异步结果(会阻塞3秒)
        String result = futureTask.get();
        System.out.println("任务执行结束,result====>" + result);
    }

}

上面代码带给我们两个疑问:

  • 为什么能把Callable塞到FutureTask中?
  • 为什么能把FutureTask塞到Thread中?

其实归根到底,是我们还不了解FutureTask到底是啥。先来看一下FutureTask的继承体系:

我们发现FutureTask实现了Runnable,所以它能塞到Thread中也就不奇怪了,毕竟也算是任务类嘛。但把Callable塞到FutureTask,然后又把FutureTask塞到Thread中,这是搞什么名堂啊,想玩俄罗斯套娃吗?This is China ya!

以上种种疑问,后面分几个小节回答。就目前来看,FutureTask有以下2个特征:

  • 能包装Runnable/Callable(上面案例代码只演示包装了Callable),但本身却又实现了Runnable接口,即本质是Runnable
  • 既然是Runnable,所以FutureTask能作为任务被Thread执行,但诡异的是FutureTask#get()可以获取结果

也就是说,FuntureTask本质上和Runnable/Callable一样是任务类,但却能通过它获取执行结果。所以FutureTask = 任务(Task) + 异步结果(Future),正如它的名字一样,FutureTask具有双重属性。不过需要注意的是,FutureTask并不直接包裹任务代码,而是通过接收Runnable/Callable来实现任务的封装。

FutureTask如何包装Runnable/Callable

我们先来看FutureTask如何包装Runnable/Callable。但在此之前,需要先验证FutureTask确实可以包装Runnable/Callable。

通过上面的验证,我们大胆猜测FutureTask内部有两个成员变量分别接收Runnable和Callable类型的任务。

然而,不好意思,只有一个Callable,你要不要?

于是你的心态炸了,Runnable确实传进去了呀。难道Runnable继承了Callable?

我去,这两货完全没关系,而且Callable是JDK1.5出来的,Runnable从JDK1.0就在了。

事情开始变得有意思起来了...

上面是通过FutureTask构造器传入Runnable/Callable的,解铃还须系铃人,所以我们去看看FutureTask的构造器吧:

不愧是大佬,原来是这样啊!

FutureTask内部维护Callable类型的成员变量,对于Callable任务,直接赋值即可:

而对于Runnable任务,需要先调用Executors#callable()把Runnable先包装成Callable

Executors#callable()用到了适配器模式:

而RunnableAdapter实现了Callable接口,所以包装后的RunnableAdapter可以赋值给FutureTask.callable。

也就是说:

  • Callable --> 直接赋值给FutureTask内部的callable字段
  • Runnable --> 通过Executors.callable(runnable) 把Runnable塞到RunnableAdapter --> 把包装后的RunnableAdapter赋值给callable字段

就这样,Runnable和Callable都被装进了FutureTask!

但是,你有没有思考过,FutureTask为什么要大费周章去包装传统的任务类呢?阅读下面小节,然后尝试得出答案。

Runnable和Callable的返回值问题

众所周知,Callable#call()是有返回值的,而Runnable#run()没有。它们都包装成FutureTask后,一个有返回值,一个没返回值,怎么处理呢?

如果我是FutureTask的设计者,我肯定要设计成有返回值的,毕竟Callable.call()明明有返回值,你总不能硬生生丢掉吧。至于Runnable.run()确实没返回值,但也好办,搞个假的返回即可。

这一步是在RunnableAdapter做的:

所以等到Thread执行FutureTask时,会先取出FutureTask.callable,然后调用callable.call():

  • 如果是真的Callable,调用Callable.call()会返回真实的result
  • 如果是Runnable包装的RunnableAdapter,会返回事先传入的result

这也是上面的程序中,我为什么要多传一个参数的原因:

FutureTask在包装Runnable时允许传入一个默认值,用来作为“假的result”

没办法,姑且就叫补偿性兼容吧。

需要注意的是,整个过程Thread完全不知道自己执行的是什么,它只知道有个Runnable类型的任务(FutureTask)通过构造器丢进来了,至于FutureTask底层包装的是Callable还是Runnable,who cares?

所以说,FutureTask在这方面设计得确实巧妙,完全屏蔽了Runnable和Callable的区别,甚至统一了返回值问题...大大提高了通用性,一个字,绝!

回答上一节问题:为什么FutureTask要大费周章去包装Runnable和Callable呢?为了一统这混乱的武林呀!不管你是黑猫(Runnable)白猫(Callable),不管你有没有尾巴(返回值),能抓老鼠(执行任务)就行,这样就可以在更高的层面把任务类统一起来!

你会在后面线程池章节看到这种做法的好处!

FutureTask是如何被Thread执行的

我相信大家最关心的还是FutureTask如何被执行。

比如Runnable的执行过程是:

总之,Thread申请到系统线程后会从自己的run()开始执行。

上面说过,FutureTask也是Runnable类型的,所以可以传入Thread执行。那么,它的执行流程应该也符合上图,只不过target变成了FutureTask,所以target.run()就是futureTask.run():

结果最终存哪呢?

这个outcome,也是FutureTask的一个成员变量,翻译过来就是“执行结果”:

难怪上面说 FutureTask = 任务 + 结果。

至此,讲完了FutureTask怎么来的,怎么被执行的,也知道了执行结果被存到它内部的outcome字段。剩下最后一个问题:怎么获取结果呢?

FutureTask#get()即可,这是一个阻塞方法。

为什么get()是阻塞的?

在FutureTask中定义了很多任务状态:

  • 刚创建
  • 即将完成
  • 完成
  • 抛异常
  • 任务取消
  • 任务即将被打断
  • 任务被打断

这些状态的设置意义在哪?

一个任务,有时可能非常耗时。而当用户使用futureTask.get()时,必然是希望获取最终结果的。如果FutureTask不帮我们阻塞,就有可能获取空结果。此时为了获取最终结果,用户不得不在外部自己写阻塞程序...哦?你觉得你写得出来?

所以,get()内部会判断当前任务的状态,只有当任务完成才返回,否则FutureTask替你完成阻塞操作。

但是线程可不能一直阻塞在那,而且从实际表现看,我们最终得到了结果,所以中间必然经历类似唤醒的操作。怎么做到的?

秘密就在awaitDone():

具体流程就不带大家读了,挺麻烦的,懂了也没有太大意义,有兴趣可以再百度百度。

核心的就是 for循环 + LockSupport。

public class ParkTest {

    @Test
    public void testPark() throws InterruptedException {
        // 存储线程
        List<Thread> threadList = new ArrayList<>();

        // 创建5个线程
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                System.out.println("我是" + Thread.currentThread().getName() + ", 我开始工作了~");
                LockSupport.park(this);
                System.out.println("我是" + Thread.currentThread().getName() + ", 我又活过来了~");
            });
            thread.start();
            threadList.add(thread);
        }

        Thread.sleep(3 * 1000L);
        System.out.println("====== 所有线程都阻塞了,3秒后全部恢复了 ======");

        // unPark()所有线程
        for (Thread thread : threadList) {
            LockSupport.unpark(thread);
        }

        // 等所有线程执行完毕
        Thread.sleep(3 * 1000L);
    }

}

也就是说,调用get()后,如果任务状态不是NORMAL(已完成),线程就会在一个死循环中park(),等有了结果再unpark()并往下走:

取出outcome返回:

网上有很多分析FutureTask源码的博客,大家感兴趣可以自行了解

FutureTask魔术解密

文章开头举了个例子:

FutureTask就像个魔术盒,刚开始是空的,你抱着它走了几步,再打开时里面竟然出现了一个苹果。

乍一听确实神奇,但深入理解源码后就会发现这是一个小把戏。

接下来,就是见证奇迹的时刻。

线程池和Thread本质是一样的,这里用线程池演示,大家不要觉得陌生。

亦真亦假的异步结果

往线程池submit一个Callable,返回了一个Future(本质是FutureTask),我们可以通过Future#get()获取线程池的执行结果:

仔细观察:

  • 返回的FutureTask包含了刚才丢进去的Callable,所以FutureTask本质是对任务类的包装
  • 由于是耗时任务,此时result.outcome目前仍为null

你之所以觉得FutureTask神奇,有一大半原因在于你被错误引导了。在你眼里,Executor#submit()必然是这样的:

public Result submit(Task t) {
    // 这是耗时操作
    ...
    // 返回结果
    return result;
}

你笃信的真理是,得到结果 = 任务结束。只有任务结束才会返回结果,如果结果一开始为null,便不会再发生改变。

真的是这样吗?

实际上,返回的futureTask并不是真正的结果,它内部持有的outcome才指向真正的结果。

大家都知道Java中存在值引用,假设我在堆内存创建了一个对象 User user = new User(); 并设置 User userCopy = user,随后把user返回给你,userCopy我自己保存着。

一开始user.getName()确实为null,但是一分钟后我调用userCopy.setName("bravo1988"),随后你再次调用user.getName()就会发现name字段凭空出现了。

你可以把“一分钟后userCopy.setName('bravo1988')”当做耗时操作,这个操作恰恰是在返回user后我通过userCopy做的。同理,返回futureTask后,任务并没有结束,在不确定的未来会产生结果,这个结果就是outcome执行的对象。

所以,开启线程后立即返回的futureTask是真的结果吗?肯定不是啊,因为耗时任务还在执行中呢,你咋就出结果了?

但能说它是假的吗?显然也不能啊,因为过一会儿你就能通过futureTask.get()得到最终结果了...

何时调用futureTask.get()

之前说过了,用户调用get()必然是想到得到最终结果的,所以为了保证一定能得到结果,JDK把FutureTask#get()设计成阻塞的。如果运气好,get()的时候可能任务刚好结束于是立即返回。如果运气不好,可能要等很久。所以,什么时候调用FutureTask#get()呢?

个人建议不要立即调用get(),否则程序完全没有发挥异步优势,由异步阻塞变成同步阻塞。

好不容易开启多线程,当然应该发挥多线程的优势:

public class AsyncAndWaitTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 单线程的线程池
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        // 为了看清楚点,我把Callable提出来赋值
        Callable<String> callable = () -> {
            System.out.println(Thread.currentThread().getName() + "========>正在执行");
            Thread.sleep(3 * 1000L);
            System.out.println("3秒");
            return "success";
        };

        // 提交Callable任务
        Future<String> result = executorService.submit(callable);
//        Thread.sleep(1000L);
//        System.out.println("1秒");
//        Thread.sleep(1000L);
//        System.out.println("2秒");
//        Thread.sleep(1000L);
        System.out.println("result=" + result.get());
    }

}

关于同步与异步、阻塞与非阻塞的概念解释,后面再解释。

FuturTask是一个非常重要的类,后面在线程池章节还会发挥巨大作用。另外,JDK1.8提供了CompletableFuture,在异步任务编排方面更加实用,我们会在后面章节介绍。

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

 

  • 28
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
FutureTask是一个实现了RunnableFuture接口的类,它继承了Runnable和Future接口。因此,FutureTask既可以被当作一个Runnable来使用,也可以被当作一个Future来使用。 FutureTask实现了Future接口,完成了对Future接口的基本实现。除了实现了Future接口以外,FutureTask还实现了Runnable接口,这意味着它可以交由Executor执行,也可以直接用线程调用执行(futureTask.run())。 FutureTask具有三种执行状态:未启动、已启动和已完成。未启动指的是在调用run()方法之前,FutureTask处于未启动状态。已启动指的是FutureTask对象的run方法正在执行过程中,FutureTask处于已启动状态。已完成指的是FutureTask正常执行结束,或者被取消,或者执行过程中抛出异常而导致中断而结束。 在ThreadPoolExecutor的submit方法中,返回的是一个Future的实现,而这个实现就是FutureTask的一个具体实例。FutureTask帮助实现了具体的任务执行,并与Future接口中的get方法关联起来。 总结起来,FutureTask是一个能够同时担任Runnable和Future角色的类,它可以作为一个任务提交给线程池执行,也可以通过get方法来获取任务执行的结果。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [【第十二篇】Java 线程池FutureFutureTask【重点】](https://blog.csdn.net/weixin_42039228/article/details/123198358)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [FutureTask简介](https://blog.csdn.net/u014516601/article/details/125123415)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值