前面介紹瞭如何利用Runnable接口構建線程任務,該方式確實方便了線程代碼的複用與共享,然而Runnable不像公共方法那樣有返回值,也就沒法將線程代碼的處理結果傳給外部,形成外部既不知曉該線程是否已經執行完畢,也不瞭解該線程的運算結果是什麼,總之沒法跟蹤分線程的行動蹤影。這裏顯然是不完美的,調用方法都有返回值,爲什麼經過Runnable啓動線程就沒法得到返回值呢?爲此Java又提供了另外一種開啓線程的方式,即利用Callable接口構建任務代碼,實現該接口須要重寫call方法,call方法相似run方法,一樣存放着開發者定義的任務代碼。不一樣的是,run方法沒有返回值,而call方法支持定義輸出參數,從而在設計上保留了分線程在運行結束時返回結果的可能性。
Callable是個泛型接口,它的返回值類型在外部調用時指定,要想建立一個Callable實例,既能經過定義完整的新類來實現,也能經過普通的匿名內部類實現。舉個簡單的應用例子,好比但願開啓分線程生成某個隨機數,並將隨機數返回給主線程,則採起匿名內部類方式書寫的Callable定義代碼以下所示:html
// 定義一個Callable代碼段,返回100之內的隨機整數 // 第一種方式:採起匿名內部類方式書寫 Callable<Integer> callable = new Callable<Integer>() { @Override public Integer call() { // 返回值爲Integer類型 int random = new Random().nextInt(100); // 獲取100之內的隨機整數 // 如下打印隨機很多天志,包括當前時間、當前線程、隨機數值等信息 PrintUtils.print(Thread.currentThread().getName(), "任務生成的隨機數="+random); return random; } };
因爲Callable又是個函數式接口,所以可以利用Lambda表達式來簡化匿名內部類,因而掐頭去尾後的Lambda表達式代碼示例以下:dom
// 第二種方式:採起Lambda表達式書寫 Callable<Integer> callable = () -> { int random = new Random().nextInt(100); PrintUtils.print(Thread.currentThread().getName(), "任務生成的隨機數="+random); return random; };
由於獲取隨機數的關鍵代碼僅有一行,因此徹底能夠進一步精簡Lambda表達式的代碼,壓縮冗餘後只有下面短小精悍的一行代碼:ide
// 第三種方式:進一步精簡後的Lambda表達式 Callable<Integer> callable = () -> new Random().nextInt(100);
有了Callable實例以後,還須要引入將來任務FutureTask把它包裝一下,由於只有FutureTask才能真正跟蹤任務的執行狀態。如下是FutureTask的主要方法說明:
run:啓動將來任務。
get:獲取將來任務的執行結果。
isDone:判斷將來任務是否執行完畢。
cancel:取消將來任務。
isCancelled:判斷將來任務是否取消。
如今結合Callable與FutureTask,串起來運行一下擁有返回值的將來任務,首先把Callable實例填進FutureTask的構造方法,由此獲得一個將來任務的實例;接着調用將來任務的run方法啓動該任務,最後調用將來任務的get方法獲取任務的執行結果。根據以上步驟編寫的將來任務代碼以下所示:函數
// 根據代碼段實例建立一個將來任務 FutureTask<Integer> future = new FutureTask<Integer>(callable); future.run(); // 運行將來任務 try { Integer result = future.get(); // 獲取將來任務的執行結果 PrintUtils.print(Thread.currentThread().getName(), "主線程的執行結果="+result); } catch (InterruptedException | ExecutionException e) { // get方法會一直等到將來任務的執行完成,因爲等待期間可能收到中斷信號,所以這裏得捕捉中斷異常 e.printStackTrace(); }
運行上述的任務調用代碼,觀察到的任務日誌以下:線程
16:48:53.363 main 任務生成的隨機數=11 16:48:53.422 main 主線程的執行結果=11
有沒有發現什麼不對勁的地方?第一行日誌是在Callable實例的call方法中打印的,第二行日誌是在主線程得到返回值後打印的,但是從日誌看,兩行日誌都由main線程也就是主線程輸出的,說明將來任務仍然由主線程執行,而非由分線程執行。
那麼如何才能開啓分線程來執行將來任務呢?固然還得讓Thread類親自出馬了,就像使用分線程執行Runnable任務那樣,一樣要把Callable實例放入Thread的構造方法當中,而後再調用線程實例的start方法方可啓動線程任務。因而添加線程類以後的將來任務代碼變成了下面這樣:設計
// 根據代碼段實例建立一個將來任務 FutureTask<Integer> future = new FutureTask<Integer>(callable); // 把將來任務放入新建立的線程中,並啓動分線程處理 new Thread(future).start(); try { Integer result = future.get(); // 獲取將來任務的執行結果 PrintUtils.print(Thread.currentThread().getName(), "主線程的執行結果="+result); } catch (InterruptedException | ExecutionException e) { // get方法會一直等到將來任務的執行完成,因爲等待期間可能收到中斷信號,所以這裏得捕捉中斷異常 e.printStackTrace(); }
運行上面的將來任務代碼,觀察到如下的程序日誌:日誌
16:49:49.816 Thread-0 任務生成的隨機數=38 16:49:49.820 main 主線程的執行結果=38
從日誌中的Thread-0名稱可知,此時的將來任務總算交由分線程執行了。htm
更多Java技術文章參見《Java開發筆記(序)章節目錄》blog