異步編程是一種對 CPU 資源更高效的編程方式,也是提高系統吞吐量的一個非常不錯的選擇。很多人會認為所謂的異步不就是多線程嗎?
但實際上這句話只能說對一半,沒錯,異步是通過多線程來實現的,但我們 Java 中的異步編程卻絕不僅僅只是多線程,它還包括對任務執行狀態的監控、隨時可以選擇性的中斷任務的執行以及獲取任務執行的返回結果。
Java 的並發包下為我們提供了一整套完善的異步任務框架,包括任務的定義、任務的提交、線程的創建與任務分配、監控任務狀態、取消任務等等,絕不僅僅局限於多線程的簡單創建與啟動。
簡單介紹與使用
下面我們先簡單介紹異步框架中的相關接口所代表的作用與含義,接着我簡單的編寫一個 demo 應用下我們異步框架。
1、任務的抽象
我們使用接口 Runnable 與 Callable 抽象的描述一個任務,前者相信大家已經非常的熟悉了,后者我們見的不多,但其實也是一個很簡單的接口,與 Runable 接口一樣也是一個函數式接口,內部定義一個 call 方法。
相比於 Runnable,除了內部定義的方法名稱不同外,call 方法還要求調用結束后返回一個結果,至於返回的結果是什么,取決於你的實現類,總的來說,兩者差別不大。
2、任務的執行
Executor 接口抽象了任務的執行者,所有的任務都可以向這里進行提交,Executor 會負責創建線程並啟動線程,執行任務。
Executor 接口的定義也是非常簡單的,只有一個 execute 執行方法。
public interface Executor {
void execute(Runnable command);
}
ExecutorService 接口繼承了接口 Executor 並新增了更多的任務執行必須的方法,例如:
- void shutdown();
- List
shutdownNow(); - boolean isShutdown();
-
Future submit(Callable task); -
Future submit(Runnable task, T result); - Future<?> submit(Runnable task);
- invokeAll,invokeAny等
這些方法我們等會會深入去分析它們,這里大家只要有個印象就好,ExecutorService 允許我們將任務進行提交,它會統一地並在合適地時候創建線程、執行任務。
3、任務的監控
Future 接口用於監控我們的任務執行狀態,是已提交但未執行,或是已取消,亦或是已完成。Future 接口中定義的方法我們也不妨列舉部分感受一下:
- boolean cancel(boolean mayInterruptIfRunning);
- boolean isCancelled();
- boolean isDone();
- V get()
細心的同學可能已經發現了,任務的監控 Future 將在任務的提交成功后返回,也就是當你成功的調用 submit 方法之后,ExecutorService 將為你返回一個 Future 接口實例供你監控剛剛提交的任務執行狀態。
下面我們看一個簡單的 demo,用於演示基本的任務提交與執行。
我們通過 Executors 的工廠方法獲取一個單線程的任務執行者,接着我們可以向這個任務執行者提交任務,當然這里簡化了代碼,使用了 Lambda 表達式,我們分別提交了兩個任務,並從 submit 方法的返回得到了任務的監控者 Future 實例。
接着,我們也就可以通過 Future 來得知任務執行的狀態。
總的來說,異步任務給我們帶來的好處是什么呢?我覺得最重要的一點就是「便捷」。
我只需要將我的任務提交就好了,不再關心如何如何創建線程,啟動線程等等細節,我也不再像以前一樣,線程啟動后根本不知道有沒有執行,我手里有 Future,我可以隨時的監控任務的執行情況。
另外,異步任務框架還有一點非常的不錯,那就是性能,它可以依賴線程池,減少線程創建和銷毀的開銷,這一切都將隨着 jdk 的迭代而不斷的優化,而我們在使用上根本不用關心,我只關心我的任務該怎么寫,至於任務怎么執行,如何高效低能耗,交給你異步框架了。
基本的實現原理
ExecutorService 繼承了 Executor 並新增了一些接口方法,這些方法數量還不少,而有些方法是很通用的,亦或是有些方法子類用不到,這你不能要求每一個子類實現者都實現了這些方法。
所以,向下又有了一層抽象,AbstractExecutorService 實現了 ExecutorService 並完成了很多方法的默認實現。后者只需要繼承 AbstractExecutorService 並重寫自己需要重寫的方法即可成為一個「異步任務的執行者」。
但是如下的方法 AbstractExecutorService 是沒有做默認實現的,需要你子類自行實現。原因也很簡單,因為這些方法不具備通用的邏輯,涉及到具體實現者內部使用的資源釋放,鎖資源競爭以及隊列資源的使用等,所以不太適合做抽象。
public void shutdown()
public List<Runnable> shutdownNow()
public boolean isShutdown()
public boolean isTerminated()
public boolean awaitTermination(long timeout, TimeUnit unit)
public void execute(Runnable command)
那我們就簡單點吧,直接從任務的提交開始看。
submit 主要有三種重載:
Future<?> submit(Runnable task)
Future
submit(Runnable task, T result) Future
submit(Callable task)
因為任務的抽象表示主要有兩種,一種是 Runnable,一種是 Callable,所以需要提供對兩種不同任務類型的抽象提交。我們以其中一個重載進行分析即可,這里我們采用第一個重載方法:
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}
RunnableFuture 繼承了 Runnable 和 Future,標識這是一個可執行的、可監控的任務。而 newTaskFor 返回的是 FutureTask (RunnableFuture的一種實現類)。
而我們也不妨看看這個 FutureTask 內部都有些哪些成員:
state 和它可取的這些值共同描述了當前任務的執行狀態,是剛開始執行,還是正在執行中,還是正常結束,還是異常結束,還是被取消了,都由這個 state 來體現。
callable 代表當前正在執行的工作內容,這里說一下為什么只有 Callable 類型的任務,因為所有的 Runnable 類型任務都會被事先轉換成 Callable 類型,我覺得主要是統一和抽象實現吧。
outcome 是任務執行結束的返回值,runner 是正在執行當前任務的線程,waiters 是一個簡單的單鏈表,維護的是所有在任務執行結束之前嘗試調用 get 方法獲取執行結果的線程集合。當任務執行結束自當喚醒隊列中所有的線程。
除此之外,還有一個稍顯重要的方法,就是 run 方法,這個方法會在任務開始時由 ExecutorService 調用,這是一個很核心的方法,雖然方法體有點長,但是邏輯簡單,我們大體上概括下。
- 如果任務已經開始將退出方法邏輯的執行
- 調度任務執行,調用 call 方法
- 調用成功將保存結果,異常則將保存異常信息
- 處理中斷
其他的方法就不去看了,也比較多,還算是簡單的,如果有所想法,也歡迎你和我探討交流。
當我們回到 submit 方法時,其實就只剩下一個 execute 方法了,execute 方法是有點復雜的,也稍繁瑣,其中也涉及了一些線程池的概念,我們在下一篇分析線程池的時候再作詳盡分析了。
這里你只要知道,execute 會根據線程池中可用線程的數量,分配並選擇一個線程執行我們的任務即可。其他的一些細節我們后續再作討論。
關於異步任務我們這里作了簡單的介紹了,總體上你應該對 Java 的異步編程體系有一個認知了,細節之處並沒有很多,因為大多會涉及一些線程池的概念,我們還未介紹。
所以,后續也會結合線程池以及 Java8 新增的組合異步再作分析。