vlambda博客
学习文章列表

面试官:异步编程Future项目中用到过吗?


在学习之前,让我们再思考一下关于‘异步编程’,它在什么背景下被广泛使用。
 
首先,是基于硬件的发展,尤其是多核处理器,在编写程序的时候,我们开始关注如何充分发挥机器本身的性能,来达到更大的吞吐量。常见的做法核心是使用多线程来实现。
 
其次是基于互联网发展的特点,服务化的架构使得API开始分散,网络中的请求呈现多而短小的特点。这既包括个体公司内部的请求层面,也包括由多个互联网公司构成的大的互联网服务层面。这样总的表现就是从多个‘数据源’获取数据,然后‘整合’,提供给最终的用户。
 
这个很好理解,假如我们打开哔哩哔哩的首页



我们肉眼可见(从上到下、从左到右)的可以有用户信息服务、用户消息服务、轮播图服务、视频列表服务,事实上是远不止这些。这里表明了网络应用“混聚”的特点。
 
拿视频列表服务来说,它本身在背后又调用了其他服务,流程可能是这样的:
 
它需要调用服务获取当前的视频列表、调用用户的资料(比如性别、年龄)、调用用户的历史浏览记录视频并提取关键字、然后通过以上的数据调用用户推荐服务,最终返回给用户界面显示的视频。
 
由此可见,要实现视频列表服务,需要调用多个不同的服务。但是我们在这个过程中并不希望因为等待其中的一个服务而阻塞服务调用链。比如为了获取当前视频列表,而阻塞调用用户资料。
 
因此,在这里我们想要实现一种并发设计(由于多核处理器的出现,我们也可以说能以并发的方式将这个服务实现,因为它只有三个服务,至于为什么不以并行思路去设计,后面系列文将会有说明)。我们不想由于程序的某个远程服务调用或者数据库查询而阻塞当前服务线程,充分利用CPU实现更高的吞吐。
 
正文来了,那我们可以如何实现这样一种并发的设计呢?
或许这样来说更直白:如何并发(非阻塞)地调用这样的服务?
 
其一需要将请求服务任务放在单独的线程中,而不至于阻塞当前线程;
其二,我们需要知道线程执行的返回结果,比如我们需要视频列表服务或者用户资料返回的数据。
 
嘿,Future正是用来处理这样的情况!
 
Futrue在jdk5被引入,它的本质是包装一个对于执行结果的引用,在逻辑处理完毕之后将这个引用返回给调用方。要使用Ftrue我们通常需要把处理的任务放在一个callable中,再将这个callable交由ExecutorService即可。如示例:
 
ExecutorService executor = Executors.newCachedThreadPool();
Future<Object> future = executor.submit(new Callable<Object>() {
    public Object call() {
        return longTimeTask();
    }
});
otherthing();
try {
    Object result = future.get(1, TimeUnit.SECONDS);
} catch (ExecutionException ee) {
// 任务抛出一个异常
} catch (InterruptedException ie) {
// 当前线程在等待过程中被中断
} catch (TimeoutException te) {
// 获取结果超时
}


以上的代码示例,可以让当前线程以并发的方式去执行耗时较长的任务,而不会阻塞当前线程的其他逻辑。接着当当前线程的逻辑必须拿到异步提交的结果才能继续运行的时候,就可以使用Future#get方法获取结果。如果此刻异步任务已经完成,则可以立刻获得结果,否则回阻塞当前线程,直到获取结果(使用了超时参数构造方法则会在超出指定时间后报错,而不至于无限等待)。
 
假设异步提交的任务需要5s,当前线程中的其它逻辑需要3s(到调用get方法之前),那么按照传统的同步调用一共需要>=8s,而使用了异步提交之后一共需要>=5s。
 
这样看起来貌似不错,恩。那想这样的方式有没有什么缺点呢?
 
假如我们提交了多个Future,这种情况下,我们很难从程序语言层面去表述它们之间的依赖关系。
 
比方我们需要实现以下几种常见的描述:
1将两个Future任务合并,同时第二个Future任务依赖于第一个Future返回的结果。
2我们要等待所有Future任务都完成。
3我们要等待所有Future任务中最快结束的任务。
4我们需要当Future任务完成之后受到一个事件通知,而不是阻塞地去等待。
 
那么面对这些情况,我们该如何优雅地继续并发之旅呢?
 
点个关注,‘异步编程实践-第二话’再见CompletableFuture!
 


【推荐阅读