Akka系列(五):Java和Scala中的Future

隨着CPU的核數的增長,異步編程模型在併發領域中的獲得了愈來愈多的應用,因爲Scala是一門函數式語言,自然的支持異步編程模型,今天主要來看一下Java和Scala中的Futrue,帶你走入異步編程的大門。html

Future

不少同窗可能會有疑問,Futrue跟異步編程有什麼關係?從Future的表面意思是將來,一個Future對象能夠看出一個未來獲得的結果,這就和異步執行的概念很像,你只管本身去執行,只要將最終的結果傳達給我就行,線程沒必要一直暫停等待結果,能夠在具體異步任務執行的時候去執行其餘操做,舉個例子:java

async work

咱們如今在執行作飯這麼一個任務,它須要煮飯,燒菜,擺置餐具等操做,若是咱們經過異步這種概念去執行這個任務,好比煮飯可能須要比較久的時間,但煮飯這個過程又不須要咱們管理,咱們能夠利用這段時間去燒菜,燒菜過程當中也可能有空閒時間,咱們能夠去擺置餐具,當電飯鍋通知咱們飯燒好了,菜也燒好了,最後咱們就能夠開始吃飯了,因此說,上面的「煮飯 -> 飯」,「燒菜 -> 菜」均可以當作一個Future的過程。git

Java中的Future

在Java的早期版本中,咱們不能獲得線程的執行結果,不論是繼承Thread類仍是實現Runnable接口,都沒法獲取線程的執行結果,因此咱們只能在線程執行的run方法裏去作相應的一些業務邏輯操做,但隨着Java5的發佈,它爲了咱們帶來了Callable和Future接口,咱們能夠利用這兩個接口的特性來獲取線程的執行結果。github

Callable接口

通俗的講,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接口來獲得一個線程執行的返回值或者是異常信息。異步

Future接口

上面說到既然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接口,它們之間的關係:

future runnable

RunnableFuture有如下兩個特色:

  • 繼承Runnable接口,仍是以run方法做爲線程執行入口,其實上面submit方法的具體實現也能夠看出,一個Callable的Task再執行的時候會被包裝成RunnableFuture,而後以FutureTask做爲實現類,執行FutureTask時,仍是執行其的run方法,只不過run方法裏面的業務邏輯是由咱們定義的call方法的內容,固然再執行run方法時,程序會自動將call方法的執行結果幫咱們包裝起來,對外部表現成一個Future對象。

  • 繼承Future接口,經過實現Future接口中的方法更新或者獲取線程的的執行狀態,好比其中的cancel(),isDone(),get()等方法。

Future程序示例與結果獲取

下面是一個簡單的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.getwithout blocking when task complete,有興趣的同窗能夠去看看。

Scala中的Future

Scala中的Future相對於Java的Future有什麼不一樣呢?我總結了一下幾點:

1.建立Future變得很容易

異步編程做爲函數式語言的一大優點,Scala對於Future的支持也是很是棒的,首先它也提供了Futrue接口,但不一樣的是咱們在構建Future對象是不用像Java同樣那麼繁瑣,而且很是簡單,舉個例子:

import scala.concurrent._ 
import ExecutionContext.Implicits.global 

val f: Future[String] = Future { "Hello World!" }

是否是很是簡單,也大大下降了咱們使用Future的難度。

2.提供真正異步的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,咱們只須要根據它的結果再作處理便可,而其餘互不依賴的操做能夠繼續執行不會阻塞。

3.強大的Future組合

前面咱們講的較多的都是單個Future的狀況,可是在真正實際應用時每每會遇到多個Future的狀況,那麼在Scala中是如何處理這種狀況的呢?

Scala中的有多種方式來組合Future,那咱們就來看看這些方式吧。

1.flatMap

咱們能夠利用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層層套着麼,因此咱們並不推薦這麼作,可是咱們須要瞭解這種方式,其餘簡潔的方式可能最終轉化成的版本也許就是這樣的。

2.for yield表達式

咱們只是把上面關於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的理解會有必定的幫助。

相關文章
相關標籤/搜索