隨著CPU的核數的增加,非同步程式設計模型在併發領域中的得到了越來越多的應用,由於Scala是一門函式式語言,天然的支援非同步程式設計模型,今天主要來看一下Java和Scala中的Futrue,帶你走入非同步程式設計的大門。
Future
很多同學可能會有疑問,Futrue跟非同步程式設計有什麼關係?從Future的表面意思是未來,一個Future物件可以看出一個將來得到的結果,這就和非同步執行的概念很像,你只管自己去執行,只要將最終的結果傳達給我就行,執行緒不必一直暫停等待結果,可以在具體非同步任務執行的時候去執行其他操作,舉個例子:
我們現在在執行做飯這麼一個任務,它需要煮飯,燒菜,擺置餐具等操作,如果我們通過非同步這種概念去執行這個任務,比如煮飯可能需要比較久的時間,但煮飯這個過程又不需要我們管理,我們可以利用這段時間去燒菜,燒菜過程中也可能有空閒時間,我們可以去擺置餐具,當電飯鍋通知我們飯燒好了,菜也燒好了,最後我們就可以開始吃飯了,所以說,上面的“煮飯 -> 飯”,“燒菜 -> 菜”都可以看成一個Future的過程。
Java中的Future
在Java的早期版本中,我們不能得到執行緒的執行結果,不管是繼承Thread類還是實現Runnable介面,都無法獲取執行緒的執行結果,所以我們只能線上程執行的run方法裡去做相應的一些業務邏輯操作,但隨著Java5的釋出,它為了我們帶來了Callable和Future介面,我們可以利用這兩個介面的特性來獲取執行緒的執行結果。
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介面:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}複製程式碼
從上面的定義,我們可以看出,兩者最大的區別就是對應的執行方法是否有返回值。Callable介面中call方法具有返回值,這便是為什麼我們可以通過Callable介面來得到一個執行緒執行的返回值或者是異常資訊。
Future介面
上面說到既然Callable介面能返回執行緒執行的結果,那麼為什麼還需要Future介面呢?因為Callable介面執行的結果只是一個將來的結果值,我們若是需要得到具體的結果就必須利用Future介面,另外Callable介面需要委託ExecutorService的submit提交任務去執行,我們來看看它是如何定義的:
<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程式示例與結果獲取
下面是一個簡單的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
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表示式
3.scala-async
另外我們可以用scala-async來組裝Futrue語句塊,示例如下:
import scala.async.Async.{async, await}
val v1 = async {
await(fut1) + await(fut2)
}
v1 foreach {
case r => println(s"the result is ${r}")
}複製程式碼
這種方式與for yield表示式有啥區別呢?其實主要有兩點:
- 表達語意更加清晰,不需要用為中間值命名
- 不需要
<-
等表示式,可減少一定的程式碼量
scala-async相關的具體資訊可以參考它的專案主頁。scala-async
總的來說Scala中的Future確實強大,在實現真正非同步的情況下,為我們提供許多方便而又簡潔的操作模式,其實比如還有Future.reduce(),Future.traverse(),Future.sequence()等方法,這些方法的具體功能和具體使用這裡就不講了,但相關的示例程式碼都會在我的示例工程裡,有興趣的同學可以去跑跑加深理解。原始碼連結
總結
這篇文章主要講解了JVM生態上兩大語言Java和Scala在非同步程式設計上的一些表現,這裡主要是Future機制,在清楚明白它的概念後,我們才能寫出更好的程式,雖然本篇文章沒有涉及到Akka相關的內容,但是Akka本身是用Scala寫的,而且大量使用了Scala中的Future,相信通過對Future的學習,對Akka的理解會有一定的幫助。