随着多核处理器的出现,提升应用程序的处理速度最有效的方式就是可以编写出发挥多核能力的软件,我们已经可以通过切分大型的任务,让每个子任务并行运行,使用线程的方式,分支/合并框架(Java 7) 和并行流(Java 8)来实现。
现在很多大型的互联网公司都对外提供了API服务,比如百度的地图,微博的新闻,天气预报等等。很少有网站或网络应用汇以完全隔离的方式工作,而是采用混聚的方式:它会使用来自多个源的内容,将这些内容聚合在一起,方便用户使用。
比如实现一个功能,你需要在微博中搜索某个新闻,然后根据当前坐标获取天气预报。这些调用第三方信息的时候,不想因为等待搜索新闻时,放弃对获取天气预报的处理,于是我们可以使用 分支/合并框架 及并行流 来并行处理,将他们切分为多个子操作,在多个不同的核、CPU甚至是机器上并行的执行这些子操作。
相反,如果你想实现并发,而不是并行,或者你的主要目标是在同一个CPU上执行几个松耦合的任务,充分利用CPU的核,让其足够忙碌,从而最大化程序的吞吐量,那么你其实真正想做的是避免因为等待远程服务的返回,或者对数据库的查询,而阻塞线程的执行,浪费宝贵的计算资源,因为这种等待时间可能会很长。Future接口,尤其是它的新版实现CompletableFuture是处理这种情况的利器。
Future接口
Future接口在java 5中被引入,设计初衷是对将来某个时刻会发生的结果进行建模。它建模了一种异步计算,返回一个执行运算结果的引用,当运算结束后,这个引用被返回给调用方。在Future中触发那些可能会耗时的操作把调用线程解放出来,让它能继续执行其他工作,不用一直等待耗时的操作完成,比如:你拿了一袋子衣服到洗衣店去洗衣服,洗衣店会给你张发票,告诉你什么时候会洗好,然后你就可以去做其他的事了。Future的另一个优点是它比更底层的Thread更容易使用。使用Future只需要讲耗时的操作封装在一个Callable对象中,再将它提交给ExecutorService就可以了。 Java 8之前使用Future的例子:
public static void main(String[] args) {
//创建Executor-Service,通过他可以向线程池提交任务
ExecutorService executor = Executors.newCachedThreadPool();
//向executor-Service提交 Callable对象
Future<Double> future = executor.submit(new Callable<Double>() {
@Override
public Double call() throws Exception {
//异步的方式执行耗时的操作
return doSomeLongComputation();
}
});
//异步时,做其他的事情
doSomethingElse();
try{
//获取异步操作的结果,如果被阻塞,无法得到结果,那么最多等待1秒钟之后退出
Double result = future.get(1, TimeUnit.SECONDS);
System.out.print(result);
} catch (InterruptedException e) {
System.out.print("计算抛出一个异常");
} catch (ExecutionException e) {
System.out.print("当前线程在等待过程中被中断");
} catch (TimeoutException e) {
System.out.print("future对象完成之前已过期");
}
}
public static Double doSomeLongComputation() throws InterruptedException {
Thread.sleep(1000);
return 3 + 4.5;
}
public static void doSomethingElse(){
System.out.print("else");
}
这种方式可以再ExecutorService以并发的方式调用另外一个线程执行耗时的操作的同时,去执行一些其他任务。接着到已经没有任务运行时,调用它的get方法来获取操作的结果,如果操作完成,就会返回结果,否则会阻塞你的线程,一直到操作完成,返回响应的结果。
CompletableFuture
在java 8 中引入了CompletableFuture类,它实现了Future接口,使用了Lambda表达式以及流水线的思想,通过下面这个例子进行学习,比如:我们要做一个商品查询,根据折扣来获取价格。