随着CPU的核数的增长,异步编程模型在并发领域中的获得了愈来愈多的应用,因为Scala是一门函数式语言,自然的支持异步编程模型,今天主要来看一下Java和Scala中的Futrue,带你走入异步编程的大门。html
不少同窗可能会有疑问,Futrue跟异步编程有什么关系?从Future的表面意思是将来,一个Future对象能够看出一个未来获得的结果,这就和异步执行的概念很像,你只管本身去执行,只要将最终的结果传达给我就行,线程没必要一直暂停等待结果,能够在具体异步任务执行的时候去执行其余操做,举个例子:java
咱们如今在执行作饭这么一个任务,它须要煮饭,烧菜,摆置餐具等操做,若是咱们经过异步这种概念去执行这个任务,好比煮饭可能须要比较久的时间,但煮饭这个过程又不须要咱们管理,咱们能够利用这段时间去烧菜,烧菜过程当中也可能有空闲时间,咱们能够去摆置餐具,当电饭锅通知咱们饭烧好了,菜也烧好了,最后咱们就能够开始吃饭了,因此说,上面的“煮饭 -> 饭”,“烧菜 -> 菜”均可以当作一个Future的过程。git
在Java的早期版本中,咱们不能获得线程的执行结果,不论是继承Thread类仍是实现Runnable接口,都没法获取线程的执行结果,因此咱们只能在线程执行的run方法里去作相应的一些业务逻辑操做,但随着Java5的发布,它为了咱们带来了Callable和Future接口,咱们能够利用这两个接口的特性来获取线程的执行结果。github
通俗的讲,Callable接口也是一个线程执行类接口,那么它跟Runnable接口有什么区别呢?咱们先来看看它们两个的定义:编程
1.Callable接口:并发
@FunctionalInterface public interface Callable<V> { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */ V call() throws Exception; }
2.Runnable接口:less
@FunctionalInterface public interface Runnable { public abstract void run(); }
从上面的定义,咱们能够看出,二者最大的区别就是对应的执行方法是否有返回值。Callable接口中call方法具备返回值,这即是为何咱们能够经过Callable接口来获得一个线程执行的返回值或者是异常信息。异步
上面说到既然Callable接口能返回线程执行的结果,那么为何还须要Future接口呢?由于Callable接口执行的结果只是一个未来的结果值,咱们如果须要获得具体的结果就必须利用Future接口,另外Callable接口须要委托ExecutorService的submit提交任务去执行,咱们来看看它是如何定义的:async
<T> Future<T> submit(Callable<T> task); public <T> Future<T> submit(Callable<T> task) { if (task == null) throw new NullPointerException(); RunnableFuture<T> ftask = newTaskFor(task); execute(ftask); return ftask; }
从submit的方法定义也能够看出它的返回值是一个Future接口类型的值,这里实际上是RunnableFuture接口,这是一个很重要的接口,咱们来看一下它的定义:异步编程
public interface RunnableFuture<V> extends Runnable, Future<V> { /** * Sets this Future to the result of its computation * unless it has been cancelled. */ void run(); }
这个接口分别继承了Runnable和Future接口,而FutureTask又实现了RunnableFuture接口,它们之间的关系:
RunnableFuture有如下两个特色:
继承Runnable接口,仍是以run方法做为线程执行入口,其实上面submit方法的具体实现也能够看出,一个Callable的Task再执行的时候会被包装成RunnableFuture,而后以FutureTask做为实现类,执行FutureTask时,仍是执行其的run方法,只不过run方法里面的业务逻辑是由咱们定义的call方法的内容,固然再执行run方法时,程序会自动将call方法的执行结果帮咱们包装起来,对外部表现成一个Future对象。
继承Future接口,经过实现Future接口中的方法更新或者获取线程的的执行状态,好比其中的cancel(),isDone(),get()等方法。
下面是一个简单的Future示例,咱们先来看一下代码:
ExecutorService es = Executors.newSingleThreadExecutor(); Future f = es.submit(() -> { System.out.println("execute call"); Thread.sleep(1000); return 5; }); try { System.out.println(f.isDone()); //检测任务是否完成 System.out.println(f.get(2000, TimeUnit.MILLISECONDS)); System.out.println(f.isDone()); //检测任务是否完成 } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } catch (TimeoutException e) { e.printStackTrace(); }
上面的代码使用了lambda表达式,有兴趣的同窗能够本身去了解下,这里咱们首先构建了一个ExecutorService,而后利用submit提交执行Callable接口的任务。
为何是Callable接口呢? 其实这里咱们并无显示声明Callable接口,这里lambda会帮咱们自动进行类型推导,首先submit接受Callable接口或Runnble接口类型做为参数,而这里咱们又给定了返回值,因此lambda能自动帮咱们推导出内部是一个Callable接口参数。
到这里咱们应该大体清楚了在Java中的获得Future,那么咱们又是如何从Future中获得咱们想要的值呢?这个结论其实很容易得出,你只须要去跑一下上面的程序便可,在利用get去获取Future中的值时,线程会一直阻塞,直到返回值或者超时,因此Future中的get方法是阻塞,因此虽然利用Future彷佛是异步执行任务,可是在某些需求上仍是会阻塞的,并非真正的异步,stackoverflow上有两个讨论说明了这个问题Future.get,without blocking when task complete,有兴趣的同窗能够去看看。
Scala中的Future相对于Java的Future有什么不一样呢?我总结了一下几点:
异步编程做为函数式语言的一大优点,Scala对于Future的支持也是很是棒的,首先它也提供了Futrue接口,但不一样的是咱们在构建Future对象是不用像Java同样那么繁琐,而且很是简单,举个例子:
import scala.concurrent._ import ExecutionContext.Implicits.global val f: Future[String] = Future { "Hello World!" }
是否是很是简单,也大大下降了咱们使用Future的难度。
前面咱们也说到,Java中的Future并非全异步的,当你须要Future里的值的时候,你只能用get去获取它,亦或者不断访问Future的状态,若完成再去取值,但其意义上便不是真正的异步了,它在获取值的时候是一个阻塞的操做,固然也就没法执行其余的操做,直到结果返回。
但在Scala中,咱们无需担忧,虽然它也提供了相似Java中获取值的方式,好比:
Future | Java | Scala |
---|---|---|
判断任务是否完成 | isDone | isCompleted |
获取值 | get | value |
可是咱们并不推荐这么作,由于这么作又回到了Java的老路上了,在Scala中咱们能够利用Callback来获取它的结果:
val fut = Future { Thread.sleep(1000) 1 + 1 } fut onComplete { case Success(r) => println(s"the result is ${r}") case _ => println("some Exception") } println("I am working") Thread.sleep(2000)
这是一个简单的例子,Future在执行完任务后会进行回调,这里使用了onComplete,也能够注册多个回调函数,但不推荐那么作,由于你不能保证这些回调函数的执行顺序,其余的一些回调函数基本都是基于onComplete的,有兴趣的同窗能够去阅读一下Future的源码。
咱们先来看一下它的运行结果:
I am working the result is 2
从结果中咱们能够分析得出,咱们在利用Callback方式来获取Future结果的时候并不会阻塞,而只是当Future完成后会自动调用onComplete,咱们只须要根据它的结果再作处理便可,而其余互不依赖的操做能够继续执行不会阻塞。
前面咱们讲的较多的都是单个Future的状况,可是在真正实际应用时每每会遇到多个Future的状况,那么在Scala中是如何处理这种状况的呢?
Scala中的有多种方式来组合Future,那咱们就来看看这些方式吧。
咱们能够利用flatMap来组合多个Future,很少说,先上代码:
val fut1 = Future { println("enter task1") Thread.sleep(2000) 1 + 1 } val fut2 = Future { println("enter task2") Thread.sleep(1000) 2 + 2 } fut1.flatMap { v1 => fut2.map { v2 => println(s"the result is ${v1 + v2}") } } Thread.sleep(2500)
利用flatMap确实能组合Future,但代码的阅读性实在是有点差,你能想象5个甚至10个map层层套着么,因此咱们并不推荐这么作,可是咱们须要了解这种方式,其余简洁的方式可能最终转化成的版本也许就是这样的。
咱们只是把上面关于flatMap的代码替换一下,看下面:
for { v1 <- fut1 v2 <- fut2 } yield println(s"the result is ${v1 + v2}")
看上去是否是比以前的方式简洁多了,这也是咱们在面对Future组合时推荐的方式,固然不得不说for yield表达式是一种语法糖,它最终仍是会被翻译成咱们常见的方法,好比flatMap,map,filter等,感兴趣的能够参考它的官方文档。for yield表达式
总的来讲Scala中的Future确实强大,在实现真正异步的状况下,为咱们提供许多方便而又简洁的操做模式,其实好比还有Future.reduce(),Future.traverse(),Future.sequence()等方法,这些方法的具体功能和具体使用这里就不讲了,但相关的示例代码都会在个人示例工程里,有兴趣的同窗能够去跑跑加深理解。源码连接
这篇文章主要讲解了JVM生态上两大语言Java和Scala在异步编程上的一些表现,这里主要是Future机制,在清楚明白它的概念后,咱们才能写出更好的程序,虽然本篇文章没有涉及到Akka相关的内容,可是Akka自己是用Scala写的,并且大量使用了Scala中的Future,相信经过对Future的学习,对Akka的理解会有必定的帮助。