Java非同步程式設計——深入原始碼分析FutureTask

公眾號_Zack說碼發表於2018-10-09

Java的非同步程式設計是一項非常常用的多執行緒技術。

之前通過原始碼詳細分析了ThreadPoolExecutor《你真的懂ThreadPoolExecutor執行緒池技術嗎?看了原始碼你會有全新的認識》。通過建立一個ThreadPoolExecutor,往裡面丟任務就可以實現多執行緒非同步執行了。

但之前的任務主要傾向於執行緒池,並沒有講到非同步程式設計方面的內容。本文將通過介紹Executor+Future框架(FutureTask是實現的核心),來深入瞭解下Java的非同步程式設計。

萬事從示例開始,我們先通過示例Demo有一個直觀的印象,再深入去了解概念與原理。

使用示例

Demo:

Java非同步程式設計——深入原始碼分析FutureTask

使用上比較簡單,

執行結果:

任務1非同步執行:0
任務2非同步執行:0
任務2非同步執行:1
...
任務2非同步執行:45
同步程式碼
任務2非同步執行:24
...
任務1非同步執行:199
任務1:執行完成
...
任務2非同步執行:199
任務2:執行完成

複製程式碼

假若你多次執行這個程式,會發現結果大大的不一樣,因為兩個任務和同步程式碼是非同步由多條執行緒執行的,列印的結果當然是隨機的。

回顧這個Demo做了什麼,

  1. 構建了一個執行緒池
  2. 往執行緒池裡面丟兩個需要執行的任務
  3. 最後獲取這兩個任務的結果

其中第二點是非同步執行兩個任務,這兩個任務和主執行緒分別是用了三個執行緒併發執行的,第三點是在主執行緒中同步等待兩個任務的結果。

很容易看出來,非同步程式設計的好處就在於可以讓不相干的任務非同步執行,不阻塞主執行緒。若是主執行緒需要非同步執行的結果,此時再去等待結果會更加高效,提高程式的執行效率。

下面來看看整個流程的實現原理。

原始碼分析

一般在實際專案中,都會有配置有自己的執行緒池,建議大家在用非同步程式設計時,配置一個專用的執行緒池,做好執行緒隔離,避免非同步執行緒影響到其他模組的工作。Demo中為了方便,直接呼叫Exectors的方法生成一個臨時的執行緒池,日常不建議使用。

我們從這個ExecutorService.submit()方法入手,看看整體實現。

Java非同步程式設計——深入原始碼分析FutureTask
ExecutorService.submit()定義一個介面。這個介面接收一個Callable引數(執行的任務),返回一個Future(計算結果)。

Callable,相當於一個需要執行的任務。它不接收任何引數,可以返回結果,可以丟擲異常。相類似的還有Runnable,它也是不接收,不同點在於它不返回結果,也不拋異常,異常需要在任務內部處理。總結來說Callable更像一個方法的呼叫,Runnable則是一個不需要理會結果的呼叫。在JDK 8以後,它們都可以通過Lamda表示式寫法去替代內部類的寫法(詳見Demo)。

Future,一個非同步計算的結果。呼叫get()方法可以得到對應的計算結果,如果呼叫時沒有非同步計算完,會阻塞等待計算的結果。同時它還提供方法可以嘗試取消任務的執行。

看回ExecutorService.submit()的實現,程式碼在實現類AbstractExecutorService中。

Java非同步程式設計——深入原始碼分析FutureTask
除了它介面的實現,還提供了兩種變形。原來介面只接收Callable引數,實現類中還新增了接收Runnable引數的。

如果看過之前寫的《你真的懂ThreadPoolExecutor執行緒池技術嗎?看了原始碼你會有全新的認識》,應該瞭解ThreadPoolExecutor執行任務是可以呼叫execute()方法的。而這裡面submit()方法則是為Callable/Runnable加多一層FutureTask,從而 使執行結果有一個存放的地方,同時也新增一個可以取消的功能。原本的execute()只能執行任務,不會返回結果的,具體實現原理可以看看之前的文章分析。

FutureTaskRunnableFuture的實現。而RunnableFuture是繼承FutureRunnable介面的,定義run()介面。

Java非同步程式設計——深入原始碼分析FutureTask
因為FutureTaskrun()介面,所以可以直接用一個Callable/Runnable建立一個FutureTask單獨執行。但這樣並沒有非同步的效果,因為沒有啟用新的執行緒去跑,而是在原來的執行緒阻塞執行的。

到這裡我們清楚知道了,submit()方法重點是利用Callable/Runnable建立一個FutureTask,然後多執行緒執行run()方法,達到非同步處理並且得到結果的效果。而FutureTask的重點則是run()方法如何持有儲存計算的結果。

FutureTask.run()

Java非同步程式設計——深入原始碼分析FutureTask
首先判斷futureTask物件的state狀態,如果不是NEW的話,證明已經開始執行過了,則退出執行。同時futureTask物件通過CAS,把當前執行緒賦值給變數runner(是Thread型別,說明物件使用哪個執行緒執行的),如果CAS失敗則退出。

外層try{}程式碼塊中,對callable判空和state狀態必須是NEW。內層try{}程式碼真正呼叫callable,開始執行任務。若執行成功,則把ran變數設為true,儲存結果在result變數中,證明已跑成功過了;若拋異常了,則設為false,result為空,並且呼叫setException()儲存異常。最後如果ran為true的話,則呼叫set()儲存result結果。

看下setException()set()的實現。

Java非同步程式設計——深入原始碼分析FutureTask
兩者的基本流程一樣,CAS置換狀態,儲存結果在outcome變數道中,但setException()儲存的結果型別固定是Throwable。另外一個不同在於最終state狀態,一個是EXCEPTION,一個是NORMAL。

這兩個方法最後都呼叫了finishCompletion()。這個方法主要是配合執行緒池喚醒下一個任務。

FutureTask.get()

從上面run()方法得知,最後執行的結果放在了outcome變數中。那最終怎麼從其中取出結果來,我們來看看get()方法。

Java非同步程式設計——深入原始碼分析FutureTask
從原始碼可知,get()方法分兩步。第一步,先判斷狀態,如果計算為完成,則需要阻塞地等待完成。第二步,如果完成了,則呼叫report()方法獲取結果並返回。

先看看awaitDone()阻塞等待完成。該方法可以選用超時功能。

Java非同步程式設計——深入原始碼分析FutureTask
在自旋的for()迴圈中,

  • 先判斷是否執行緒被中斷,中斷的話拋異常退出。
  • 然後開始判斷執行的state值,如果state大於COMPLETING,證明計算已經是終態了,此時返回終態變數。
  • state等於COMPLETING,證明已經開始計算,並且還在計算中。此時為了避免過多的CPU時間放在這個for迴圈的自旋上,程式執行Thread.yield(),把執行緒從執行態降為就緒態,讓出CPU時間。
  • 若以上狀態都不是,則證明stateNEW,還沒開始執行。那麼程式在當前迴圈現在會新增一個WaitNode,在下一個迴圈裡面呼叫LockSupport.park()把當前執行緒阻塞。當run()方法結束的時候,會再次喚醒此執行緒,避免自旋消耗CPU時間。
  • 如果選用了超時功能,在阻塞和自旋過程中超時了,則會返回當前超時的狀態。

第二步的report()方法比較簡單。

Java非同步程式設計——深入原始碼分析FutureTask

  • 如果狀態是NORMAL,正常結束的話,則把outcome變數返回;
  • 如果是取消或者中斷狀態的,則丟擲取消異常;
  • 如果是EXCEPTION,則把outcome當作異常丟擲(之前setException()儲存的型別就是Throwable)。從而整個get()會有一個異常丟擲。

總結

至此我們已經比較完整地瞭解Executor+Future的框架原理了,而FutureTask則是該框架的主要實現。下面總結下要點

  1. Executor.sumbit()方法非同步執行一個任務,並且返回一個Future結果。
  2. submit()的原理是利用Callable建立一個FutureTask物件,然後執行物件的run()方法,把結果儲存在outcome中。
  3. 呼叫get()獲取outcome時,如果任務未完成,會阻塞執行緒,等待執行完畢。
  4. 異常和正常結果都放在outcome中,呼叫get()獲取結果或丟擲異常。

更多技術文章、精彩乾貨,請關注
部落格:zackku.com
微信公眾號:Zack說碼

Java非同步程式設計——深入原始碼分析FutureTask

相關文章