JDK1.8源碼分析項目(中文注釋)Github地址:
https://github.com/yuanmabiji/jdk1.8-sourcecode-blogs
1 Future是什麼?
先舉個例子,我們平時網購買東西,下單後會生成一個訂單號,然後商家會根據這個訂單號發貨,發貨後又有一個快遞單號,然後快遞公司就會根據這個快遞單號將網購東西快遞給我們。在這一過程中,這一系列的單號都是我們收貨的重要憑證。
因此,JDK的Future就類似於我們網購買東西的單號,當我們執行某一耗時的任務時,我們可以另起一個線程異步去執行這個耗時的任務,同時我們可以幹點其他事情。當事情幹完後我們再根據future這個"單號"去提取耗時任務的執行結果即可。因此Future也是多線程中的一種應用模式。
擴展: 說起多線程,那麼Future又與Thread有什麼區別呢?最重要的區別就是Thread是沒有返回結果的,而Future模式是有返回結果的。
2 如何使用Future
前面搞明白了什麼是Future,下面我們再來舉個簡單的例子看看如何使用Future。
假如現在我們要打火鍋,首先我們要準備兩樣東西:把水燒開和準備食材。因為燒開水是一個比較漫長的過程(相當於耗時的業務邏輯),因此我們可以一邊燒開水(相當於另起一個線程),一邊準備火鍋食材(主線程),等兩者都準備好了我們就可以開始打火鍋了。
// DaHuoGuo.javapublicclassDaHuoGuo{publicstaticvoidmain(String[] args)throws Exception { FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {@Overridepublic String call()throws Exception { System.out.println(Thread.currentThread().getName() + ":" + "開始燒開水...");// 模擬燒開水耗時 Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + ":" + "開水已經燒好了...");return"開水"; } }); Thread thread = new Thread(futureTask); thread.start();// do other thing System.out.println(Thread.currentThread().getName() + ":" + " 此時開啟了一個線程執行future的邏輯(燒開水),此時我們可以幹點別的事情(比如準備火鍋食材)...");// 模擬準備火鍋食材耗時 Thread.sleep(3000); System.out.println(Thread.currentThread().getName() + ":" + "火鍋食材準備好了"); String shicai = "火鍋食材";// 開水已經稍好,我們取得燒好的開水 String boilWater = futureTask.get(); System.out.println(Thread.currentThread().getName() + ":" + boilWater + "和" + shicai + "已經準備好,我們可以開始打火鍋啦"); }}執行結果如下截圖,符合我們的預期:
從以上代碼中可以看到,我們使用Future主要有以下步驟:
新建一個Callable匿名函數實現類對象,我們的業務邏輯在Callable的call方法中實現,其中Callable的泛型是返回結果類型;然後把Callable匿名函數對象作為FutureTask的構造參數傳入,構建一個futureTask對象;然後再把futureTask對象作為Thread構造參數傳入並開啟這個線程執行去執行業務邏輯;最後我們調用futureTask對象的get方法得到業務邏輯執行結果。可以看到跟Future使用有關的JDK類主要有FutureTask和Callable兩個,下面主要對FutureTask進行源碼分析。
擴展:還有一種使用Future的方式是將Callable實現類提交給線程池執行的方式,這裡不再介紹,自行百度即可。
3 FutureTask類結構分析
我們先來看下FutureTask的類結構:
可以看到FutureTask實現了RunnableFuture接口,而RunnableFuture接口又繼承了Future和Runnable接口。因為FutureTask間接實現了Runnable接口,因此可以作為任務被線程Thread執行;此外,最重要的一點就是FutureTask還間接實現了Future接口,因此還可以獲得任務執行的結果。下面我們就來簡單看看這幾個接口的相關api。
// Runnable.java@FunctionalInterfacepublicinterfaceRunnable{// 執行線程任務publicabstractvoidrun();}Runnable沒啥好說的,相信大家都已經很熟悉了。
// Future.javapublicinterfaceFuture<V> {/** * 嘗試取消線程任務的執行,分為以下幾種情況: * 1)如果線程任務已經完成或已經被取消或其他原因不能被取消,此時會失敗並返回false; * 2)如果任務還未開始執行,此時執行cancel方法,那麼任務將被取消執行,此時返回true;TODO 此時對應任務狀態state的哪種狀態???不懂!! * 3)如果任務已經開始執行,那麼mayInterruptIfRunning這個參數將決定是否取消任務的執行。 * 這裡值得注意的是,cancel(true)實質並不能真正取消線程任務的執行,而是發出一個線程 * 中斷的信號,一般需要結合Thread.currentThread().isInterrupted()來使用。 */booleancancel(boolean mayInterruptIfRunning);/** * 判斷任務是否被取消,在執行任務完成前被取消,此時會返回true */booleanisCancelled();/** * 這個方法不管任務正常停止,異常還是任務被取消,總是返回true。 */booleanisDone();/** * 獲取任務執行結果,注意是阻塞等待獲取任務執行結果。 */V get()throws InterruptedException, ExecutionException;/** * 獲取任務執行結果,注意是阻塞等待獲取任務執行結果。 * 只不過在規定的時間內未獲取到結果,此時會拋出超時異常 */V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException;}Future接口象徵著異步執行任務的結果即執行一個耗時任務完全可以另起一個線程執行,然後此時我們可以去做其他事情,做完其他事情我們再調用Future.get()方法獲取結果即可,此時若異步任務還沒結束,此時會一直阻塞等待,直到異步任務執行完獲取到結果。
// RunnableFuture.javapublicinterfaceRunnableFuture<V> extendsRunnable, Future<V> {/** * Sets this Future to the result of its computation * unless it has been cancelled. */voidrun();}RunnableFuture是Future和Runnable接口的組合,即這個接口表示又可以被線程異步執行,因為實現了Runnable接口,又可以獲得線程異步任務的執行結果,因為實現了Future接口。因此解決了Runnable異步任務沒有返回結果的缺陷。
接下來我們來看下FutureTask,FutureTask實現了RunnableFuture接口,因此是Future和Runnable接口的具體實現類,是一個可被取消的異步線程任務,提供了Future的基本實現,即異步任務執行後我們能夠獲取到異步任務的執行結果,是我們接下來分析的重中之重。FutureTask可以包裝一個Callable和Runnable對象,此外,FutureTask除了可以被線程執行外,還可以被提交給線程池執行。
我們先看下FutureTask類的api,其中重點方法已經紅框框出。
上圖中FutureTask的run方法是被線程異步執行的方法,get方法即是取得異步任務執行結果的方法,還有cancel方法是取消任務執行的方法。接下來我們主要對這三個方法進行重點分析。
思考:
FutureTask覆寫的run方法的返回類型依然是void,表示沒有返回值,那麼FutureTask的get方法又是如何獲得返回值的呢?FutureTask的cancel方法能真正取消線程異步任務的執行麼?什麼情況下能取消?因為FutureTask異步任務執行結果還跟Callable接口有關,因此我們再來看下Callable接口:
// Callable.java@FunctionalInterfacepublicinterfaceCallable<V> {/** * Computes a result, or throws an exception if unable to do so. */V call()throws Exception;}我們都知道,Callable<V>接口和Runnable接口都可以被提交給線程池執行,唯一不同的就是Callable<V>接口是有返回結果的,其中的泛型V就是返回結果,而Runnable接口是沒有返回結果的。
思考:一般情況下,Runnable接口實現類才能被提交給線程池執行,為何Callable接口實現類也可以被提交給線程池執行?想想線程池的submit方法內部有對Callable做適配麼?
4 FutureTask源碼分析
4.1 FutureTask成員變量
我們首先來看下FutureTask的成員變量有哪些,理解這些成員變量對後面的源碼分析非常重要。
// FutureTask.java/** 封裝的Callable對象,其call方法用來執行異步任務 */private Callable<V> callable;/** 在FutureTask裡面定義一個成員變量outcome,用來裝異步任務的執行結果 */private Object outcome; // non-volatile, protected by state reads/writes/** 用來執行callable任務的線程 */privatevolatile Thread runner;/** 線程等待節點,reiber stack的一種實現 */privatevolatile WaitNode waiters;/** 任務執行狀態 */privatevolatileint state;// Unsafe mechanicsprivatestaticfinal sun.misc.Unsafe UNSAFE;// 對應成員變量state的偏移地址privatestaticfinallong stateOffset;// 對應成員變量runner的偏移地址privatestaticfinallong runnerOffset;// 對應成員變量waiters的偏移地址privatestaticfinallong waitersOffset;這裡我們要重點關注下FutureTask的Callable成員變量,因為FutureTask的異步任務最終是委託給Callable去實現的。
思考:
FutureTask的成員變量runner,waiters和state都被volatile修飾,我們可以思考下為什麼這三個成員變量需要被volatile修飾,而其他成員變量又不用呢?volatile關鍵字的作用又是什麼呢?既然已經定義了成員變量runner,waiters和state了,此時又定義了stateOffset,runnerOffset和waitersOffset變量分別對應runner,waiters和state的偏移地址,為何要多此一舉呢?我們再來看看stateOffset,runnerOffset和waitersOffset變量這三個變量的初始化過程:
// FutureTask.javastatic {try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> k = FutureTask.class; stateOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("state")); runnerOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("runner")); waitersOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("waiters")); } catch (Exception e) {thrownew Error(e); } }4.2 FutureTask的狀態變化
前面講了FutureTask的成員變量,有一個表示狀態的成員變量state我們要重點關注下,state變量表示任務執行的狀態。
// FutureTask.java/** 任務執行狀態 */privatevolatileint state;/** 任務新建狀態 */privatestaticfinalint NEW = 0;/** 任務正在完成狀態,是一個瞬間過渡狀態 */privatestaticfinalint COMPLETING = 1;/** 任務正常結束狀態 */privatestaticfinalint NORMAL = 2;/** 任務執行異常狀態 */privatestaticfinalint EXCEPTIONAL = 3;/** 任務被取消狀態,對應cancel(false) */privatestaticfinalint CANCELLED = 4;/** 任務中斷狀態,是一個瞬間過渡狀態 */privatestaticfinalint INTERRUPTING = 5;/** 任務被中斷狀態,對應cancel(true) */privatestaticfinalint INTERRUPTED = 6;可以看到任務狀態變量state有以上7種狀態,0-6分別對應著每一種狀態。任務狀態一開始是NEW,然後由FutureTask的三個方法set,setException和cancel來設置狀態的變化,其中狀態變化有以下四種情況:
NEW -> COMPLETING -> NORMAL:這個狀態變化表示異步任務的正常結束,其中COMPLETING是一個瞬間臨時的過渡狀態,由set方法設置狀態的變化;NEW -> COMPLETING -> EXCEPTIONAL:這個狀態變化表示異步任務執行過程中拋出異常,由setException方法設置狀態的變化;NEW -> CANCELLED:這個狀態變化表示被取消,即調用了cancel(false),由cancel方法來設置狀態變化;NEW -> INTERRUPTING -> INTERRUPTED:這個狀態變化表示被中斷,即調用了cancel(true),由cancel方法來設置狀態變化。4.3 FutureTask構造函數
FutureTask有兩個構造函數,我們分別來看看:
// FutureTask.java// 第一個構造函數publicFutureTask(Callable<V> callable){if (callable == null)thrownew NullPointerException();this.callable = callable;this.state = NEW; // ensure visibility of callable}可以看到,這個構造函數在我們前面舉的「打火鍋」的例子代碼中有用到,就是Callable成員變量賦值,在異步執行任務時再調用Callable.call方法執行異步任務邏輯。此外,此時給任務狀態state賦值為NEW,表示任務新建狀態。
我們再來看下FutureTask的另外一個構造函數:
// FutureTask.java// 另一個構造函數publicFutureTask(Runnable runnable, V result){this.callable = Executors.callable(runnable, result);this.state = NEW; // ensure visibility of callable}這個構造函數在執行Executors.callable(runnable, result)時是通過適配器RunnableAdapter來將Runnable對象runnable轉換成Callable對象,然後再分別給callable和state變量賦值。
注意,這裡我們需要記住的是FutureTask新建時,此時的任務狀態state是NEW就好了。
4.4 FutureTask.run方法,用來執行異步任務
前面我們有講到FutureTask間接實現了Runnable接口,覆寫了Runnable接口的run方法,因此該覆寫的run方法是提交給線程來執行的,同時,該run方法正是執行異步任務邏輯的方法,那麼,執行完run方法又是如何保存異步任務執行的結果的呢?
我們現在著重來分析下run方法:
// FutureTask.javapublicvoidrun(){// 【1】,為了防止多線程並發執行異步任務,這裡需要判斷線程滿不滿足執行異步任務的條件,有以下三種情況:// 1)若任務狀態state為NEW且runner為null,說明還未有線程執行過異步任務,此時滿足執行異步任務的條件,// 此時同時調用CAS方法為成員變量runner設置當前線程的值;// 2)若任務狀態state為NEW且runner不為null,任務狀態雖為NEW但runner不為null,說明有線程正在執行異步任務,// 此時不滿足執行異步任務的條件,直接返回;// 1)若任務狀態state不為NEW,此時不管runner是否為null,說明已經有線程執行過異步任務,此時沒必要再重新// 執行一次異步任務,此時不滿足執行異步任務的條件;if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset,null, Thread.currentThread()))return;try {// 拿到之前構造函數傳進來的callable實現類對象,其call方法封裝了異步任務執行的邏輯 Callable<V> c = callable;// 若任務還是新建狀態的話,那麼就調用異步任務if (c != null && state == NEW) {// 異步任務執行結果 V result;// 異步任務執行成功還是始遍標誌boolean ran;try {// 【2】,執行異步任務邏輯,並把執行結果賦值給result result = c.call();// 若異步任務執行過程中沒有拋出異常,說明異步任務執行成功,此時設置ran標誌為true ran = true; } catch (Throwable ex) { result = null;// 異步任務執行過程拋出異常,此時設置ran標誌為false ran = false;// 【3】設置異常,裡面也設置state狀態的變化 setException(ex); }// 【3】若異步任務執行成功,此時設置異步任務執行結果,同時也設置狀態的變化if (ran) set(result); } } finally {// runner must be non-null until state is settled to// prevent concurrent calls to run()// 異步任務正在執行過程中,runner一直是非空的,防止並發調用run方法,前面有調用cas方法做判斷的// 在異步任務執行完後,不管是正常結束還是異常結束,此時設置runner為null runner = null;// state must be re-read after nulling runner to prevent// leaked interrupts// 線程執行異步任務後的任務狀態int s = state;// 【4】如果執行了cancel(true)方法,此時滿足條件,// 此時調用handlePossibleCancellationInterrupt方法處理中斷if (s >= INTERRUPTING) handlePossibleCancellationInterrupt(s); }}可以看到執行異步任務的run方法主要分為以下四步來執行:
判斷線程是否滿足執行異步任務的條件:為了防止多線程並發執行異步任務,這裡需要判斷線程滿不滿足執行異步任務的條件;若滿足條件,執行異步任務:因為異步任務邏輯封裝在Callable.call方法中,此時直接調用Callable.call方法執行異步任務,然後返回執行結果;根據異步任務的執行情況做不同的處理:1) 若異步任務執行正常結束,此時調用set(result);來設置任務執行結果;2)若異步任務執行拋出異常,此時調用setException(ex);來設置異常,詳細分析請見4.4.1小節;異步任務執行完後的善後處理工作:不管異步任務執行成功還是失敗,若其他線程有調用FutureTask.cancel(true),此時需要調用handlePossibleCancellationInterrupt方法處理中斷,詳細分析請見4.4.2小節。這裡值得注意的是判斷線程滿不滿足執行異步任務條件時,runner是否為null是調用UNSAFE的CAS方法compareAndSwapObject來判斷和設置的,同時compareAndSwapObject是通過成員變量runner的偏移地址runnerOffset來給runner賦值的,此外,成員變量runner被修飾為volatile是在多線程的情況下, 一個線程的volatile修飾變量的設值能夠立即刷進主存,因此值便可被其他線程可見。
4.4.1 FutureTask的set和setException方法
下面我們來看下當異步任務執行正常結束時,此時會調用set(result);方法:
// FutureTask.javaprotectedvoidset(V v){// 【1】調用UNSAFE的CAS方法判斷任務當前狀態是否為NEW,若為NEW,則設置任務狀態為COMPLETING// 【思考】此時任務不能被多線程並發執行,什麼情況下會導致任務狀態不為NEW?// 答案是只有在調用了cancel方法的時候,此時任務狀態不為NEW,此時什麼都不需要做,// 因此需要調用CAS方法來做判斷任務狀態是否為NEWif (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {// 【2】將任務執行結果賦值給成員變量outcome outcome = v;// 【3】將任務狀態設置為NORMAL,表示任務正常結束 UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state// 【4】調用任務執行完成方法,此時會喚醒阻塞的線程,調用done()方法和清空等待線程鍊表等 finishCompletion(); }}可以看到當異步任務正常執行結束後,且異步任務沒有被cancel的情況下,此時會做以下事情:將任務執行結果保存到FutureTask的成員變量outcome中的,賦值結束後會調用finishCompletion方法來喚醒阻塞的線程(哪裡來的阻塞線程?後面會分析),值得注意的是這裡對應的任務狀態變化是NEW -> COMPLETING -> NORMAL。
我們繼續來看下當異步任務執行過程中拋出異常,此時會調用setException(ex);方法。
// FutureTask.javaprotectedvoidsetException(Throwable t){// 【1】調用UNSAFE的CAS方法判斷任務當前狀態是否為NEW,若為NEW,則設置任務狀態為COMPLETING// 【思考】此時任務不能被多線程並發執行,什麼情況下會導致任務狀態不為NEW?// 答案是只有在調用了cancel方法的時候,此時任務狀態不為NEW,此時什麼都不需要做,// 因此需要調用CAS方法來做判斷任務狀態是否為NEWif (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {// 【2】將異常賦值給成員變量outcome outcome = t;// 【3】將任務狀態設置為EXCEPTIONAL UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state// 【4】調用任務執行完成方法,此時會喚醒阻塞的線程,調用done()方法和清空等待線程鍊表等 finishCompletion(); }}可以看到setException(Throwable t)的代碼邏輯跟前面的set(V v)幾乎一樣,不同的是任務執行過程中拋出異常,此時是將異常保存到FutureTask的成員變量outcome中,還有,值得注意的是這裡對應的任務狀態變化是NEW -> COMPLETING -> EXCEPTIONAL。
因為異步任務不管正常還是異常結束,此時都會調用FutureTask的finishCompletion方法來喚醒喚醒阻塞的線程,這裡阻塞的線程是指我們調用Future.get方法時若異步任務還未執行完,此時該線程會阻塞。
// FutureTask.javaprivatevoidfinishCompletion(){// assert state > COMPLETING;// 取出等待線程鍊表頭節點,判斷頭節點是否為null// 1)若線程鍊表頭節點不為空,此時以「後進先出」的順序(棧)移除等待的線程WaitNode節點// 2)若線程鍊表頭節點為空,說明還沒有線程調用Future.get()方法來獲取任務執行結果,固然不用移除for (WaitNode q; (q = waiters) != null;) {// 調用UNSAFE的CAS方法將成員變量waiters設置為空if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {for (;;) {// 取出WaitNode節點的線程 Thread t = q.thread;// 若取出的線程不為null,則將該WaitNode節點線程置空,且喚醒正在阻塞的該線程if (t != null) { q.thread = null;//【重要】喚醒正在阻塞的該線程 LockSupport.unpark(t); }// 繼續取得下一個WaitNode線程節點 WaitNode next = q.next;// 若沒有下一個WaitNode線程節點,說明已經將所有等待的線程喚醒,此時跳出for循環if (next == null)break;// 將已經移除的線程WaitNode節點的next指針置空,此時好被垃圾回收 q.next = null; // unlink to help gc// 再把下一個WaitNode線程節點置為當前線程WaitNode頭節點 q = next; }break; } }// 不管任務正常執行還是拋出異常,都會調用done方法 done();// 因為異步任務已經執行完且結果已經保存到outcome中,因此此時可以將callable對象置空了 callable = null; // to reduce footprint}finishCompletion方法的作用就是不管異步任務正常還是異常結束,此時都要喚醒且移除線程等待鍊表的等待線程節點,這個鍊表實現的是一個是Treiber stack,因此喚醒(移除)的順序是"後進先出"即後面先來的線程先被先喚醒(移除),關於這個線程等待鍊表是如何成鏈的,後面再繼續分析。
4.4.2 FutureTask的handlePossibleCancellationInterrupt方法
在4.4小節分析的run方法裡的最後有一個finally塊,此時若任務狀態state >= INTERRUPTING,此時說明有其他線程執行了cancel(true)方法,此時需要讓出CPU執行的時間片段給其他線程執行,我們來看下具體的源碼:
// FutureTask.javaprivatevoidhandlePossibleCancellationInterrupt(int s){// It is possible for our interrupter to stall before getting a// chance to interrupt us. Let's spin-wait patiently.// 當任務狀態是INTERRUPTING時,此時讓出CPU執行的機會,讓其他線程執行if (s == INTERRUPTING)while (state == INTERRUPTING) Thread.yield(); // wait out pending interrupt// assert state == INTERRUPTED;// We want to clear any interrupt we may have received from// cancel(true). However, it is permissible to use interrupts// as an independent mechanism for a task to communicate with// its caller, and there is no way to clear only the// cancellation interrupt.//// Thread.interrupted();}思考:為啥任務狀態是INTERRUPTING時,此時就要讓出CPU執行的時間片段呢?還有為什麼要在義務任務執行後才調用handlePossibleCancellationInterrupt方法呢?
4.5 FutureTask.get方法,獲取任務執行結果
前面我們起一個線程在其`run`方法中執行異步任務後,此時我們可以調用`FutureTask.get`方法來獲取異步任務執行的結果。// FutureTask.javapublic V get()throws InterruptedException, ExecutionException {int s = state;// 【1】若任務狀態<=COMPLETING,說明任務正在執行過程中,此時可能正常結束,也可能遇到異常if (s <= COMPLETING) s = awaitDone(false, 0L);// 【2】最後根據任務狀態來返回任務執行結果,此時有三種情況:1)任務正常執行;2)任務執行異常;3)任務被取消return report(s);}可以看到,如果任務狀態state<=COMPLETING,說明異步任務正在執行過程中,此時會調用awaitDone方法阻塞等待;當任務執行完後,此時再調用report方法來報告任務結果,此時有三種情況:1)任務正常執行;2)任務執行異常;3)任務被取消。
4.5.1 FutureTask.awaitDone方法
FutureTask.awaitDone方法會阻塞獲取異步任務執行結果的當前線程,直到異步任務執行完成。
// FutureTask.javaprivateintawaitDone(boolean timed, long nanos)throws InterruptedException {// 計算超時結束時間finallong deadline = timed ? System.nanoTime() + nanos : 0L;// 線程鍊表頭節點 WaitNode q = null;// 是否入隊boolean queued = false;// 死循環for (;;) {// 如果當前獲取任務執行結果的線程被中斷,此時移除該線程WaitNode鍊表節點,並拋出InterruptedExceptionif (Thread.interrupted()) { removeWaiter(q);thrownew InterruptedException(); }int s = state;// 【5】如果任務狀態>COMPLETING,此時返回任務執行結果,其中此時任務可能正常結束(NORMAL),可能拋出異常(EXCEPTIONAL)// 或任務被取消(CANCELLED,INTERRUPTING或INTERRUPTED狀態的一種)if (s > COMPLETING) {// 【問】此時將當前WaitNode節點的線程置空,其中在任務結束時也會調用finishCompletion將WaitNode節點的thread置空,// 這裡為什麼又要再調用一次q.thread = null;呢?// 【答】因為若很多線程來獲取任務執行結果,在任務執行完的那一刻,此時獲取任務的線程要麼已經在線程等待鍊表中,要麼// 此時還是一個孤立的WaitNode節點。在線程等待鍊表中的的所有WaitNode節點將由finishCompletion來移除(同時喚醒)所有// 等待的WaitNode節點,以便垃圾回收;而孤立的線程WaitNode節點此時還未阻塞,因此不需要被喚醒,此時只要把其屬性置為// null,然後其有沒有被誰引用,因此可以被GC。if (q != null) q.thread = null;// 【重要】返回任務執行結果return s; }// 【4】若任務狀態為COMPLETING,此時說明任務正在執行過程中,此時獲取任務結果的線程需讓出CPU執行時間片段elseif (s == COMPLETING) // cannot time out yet Thread.yield();// 【1】若當前線程還沒有進入線程等待鍊表的WaitNode節點,此時新建一個WaitNode節點,並把當前線程賦值給WaitNode節點的thread屬性elseif (q == null) q = new WaitNode();// 【2】若當前線程等待節點還未入線程等待隊列,此時加入到該線程等待隊列的頭部elseif (!queued) queued = UNSAFE.compareAndSwapObject(this, waitersOffset, q.next = waiters, q);// 若有超時設置,那麼處理超時獲取任務結果的邏輯elseif (timed) { nanos = deadline - System.nanoTime();if (nanos <= 0L) { removeWaiter(q);return state; } LockSupport.parkNanos(this, nanos); }// 【3】若沒有超時設置,此時直接阻塞當前線程else LockSupport.park(this); }}FutureTask.awaitDone方法主要做的事情總結如下:
首先awaitDone方法裡面是一個死循環;若獲取結果的當前線程被其他線程中斷,此時移除該線程WaitNode鍊表節點,並拋出InterruptedException;如果任務狀態state>COMPLETING,此時返回任務執行結果;若任務狀態為COMPLETING,此時獲取任務結果的線程需讓出CPU執行時間片段;若q == null,說明當前線程還未設置到WaitNode節點,此時新建WaitNode節點並設置其thread屬性為當前線程;若queued==false,說明當前線程WaitNode節點還未加入線程等待鍊表,此時加入該鍊表的頭部;當timed設置為true時,此時該方法具有超時功能,關於超時的邏輯這裡不詳細分析;當前面6個條件都不滿足時,此時阻塞當前線程。我們分析到這裡,可以直到執行異步任務只能有一個線程來執行,而獲取異步任務結果可以多線程來獲取,當異步任務還未執行完時,此時獲取異步任務結果的線程會加入線程等待鍊表中,然後調用調用LockSupport.park(this);方法阻塞當前線程。直到異步任務執行完成,此時會調用finishCompletion方法來喚醒並移除線程等待鍊表的每個WaitNode節點,這裡這裡喚醒(移除)WaitNode節點的線程是從鍊表頭部開始的,前面我們也已經分析過。
還有一個特別需要注意的就是awaitDone方法裡面是一個死循環,當一個獲取異步任務的線程進來後可能會多次進入多個條件分支執行不同的業務邏輯,也可能只進入一個條件分支。下面分別舉兩種可能的情況進行說明:
情況1:當獲取異步任務結果的線程進來時,此時異步任務還未執行完即state=NEW且沒有超時設置時:
第一次循環:此時q = null,此時進入上面代碼標號【1】的判斷分支,即為當前線程新建一個WaitNode節點;第二次循環:此時queued = false,此時進入上面代碼標號【2】的判斷分支,即將之前新建的WaitNode節點加入線程等待鍊表中;第三次循環:此時進入上面代碼標號【3】的判斷分支,即阻塞當前線程;第四次循環:加入此時異步任務已經執行完,此時進入上面代碼標號【5】的判斷分支,即返回異步任務執行結果。情況2:當獲取異步任務結果的線程進來時,此時異步任務已經執行完即state>COMPLETING且沒有超時設置時,此時直接進入上面代碼標號【5】的判斷分支,即直接返回異步任務執行結果即可,也不用加入線程等待鍊表了。
4.5.2 FutureTask.report方法
在get方法中,當異步任務執行結束後即不管異步任務正常還是異常結束,亦或是被cancel,此時獲取異步任務結果的線程都會被喚醒,因此會繼續執行FutureTask.report方法報告異步任務的執行情況,此時可能會返回結果,也可能會拋出異常。
// FutureTask.javaprivate V report(int s)throws ExecutionException {// 將異步任務執行結果賦值給x,此時FutureTask的成員變量outcome要麼保存著// 異步任務正常執行的結果,要麼保存著異步任務執行過程中拋出的異常 Object x = outcome;// 【1】若異步任務正常執行結束,此時返回異步任務執行結果即可if (s == NORMAL)return (V)x;// 【2】若異步任務執行過程中,其他線程執行過cancel方法,此時拋出CancellationException異常if (s >= CANCELLED)thrownew CancellationException();// 【3】若異步任務執行過程中,拋出異常,此時將該異常轉換成ExecutionException後,重新拋出。thrownew ExecutionException((Throwable)x);}4.6 FutureTask.cancel方法,取消執行任務
我們最後再來看下FutureTask.cancel方法,我們一看到FutureTask.cancel方法,肯定一開始就天真的認為這是一個可以取消異步任務執行的方法,如果我們這樣認為的話,只能說我們猜對了一半。
// FutureTask.javapublicbooleancancel(boolean mayInterruptIfRunning){// 【1】判斷當前任務狀態,若state == NEW時根據mayInterruptIfRunning參數值給當前任務狀態賦值為INTERRUPTING或CANCELLED// a)當任務狀態不為NEW時,說明異步任務已經完成,或拋出異常,或已經被取消,此時直接返回false。// TODO 【問題】此時若state = COMPLETING呢?此時為何也直接返回false,而不能發出中斷異步任務線程的中斷信號呢??// TODO 僅僅因為COMPLETING是一個瞬時態嗎???// b)當前僅當任務狀態為NEW時,此時若mayInterruptIfRunning為true,此時任務狀態賦值為INTERRUPTING;否則賦值為CANCELLED。if (!(state == NEW && UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))returnfalse;try { // in case call to interrupt throws exception// 【2】如果mayInterruptIfRunning為true,此時中斷執行異步任務的線程runner(還記得執行異步任務時就把執行異步任務的線程就賦值給了runner成員變量嗎)if (mayInterruptIfRunning) {try { Thread t = runner;if (t != null)// 中斷執行異步任務的線程runner t.interrupt(); } finally { // final state// 最後任務狀態賦值為INTERRUPTED UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); } }// 【3】不管mayInterruptIfRunning為true還是false,此時都要調用finishCompletion方法喚醒阻塞的獲取異步任務結果的線程並移除線程等待鍊表節點 } finally { finishCompletion(); }// 返回truereturntrue;}以上代碼中,當異步任務狀態state != NEW時,說明異步任務已經正常執行完或已經異常結束亦或已經被cancel,此時直接返回false;當異步任務狀態state = NEW時,此時又根據mayInterruptIfRunning參數是否為true分為以下兩種情況:
當mayInterruptIfRunning = false時,此時任務狀態state直接被賦值為CANCELLED,此時不會對執行異步任務的線程發出中斷信號,值得注意的是這裡對應的任務狀態變化是NEW -> CANCELLED。當mayInterruptIfRunning = true時,此時會對執行異步任務的線程發出中斷信號,值得注意的是這裡對應的任務狀態變化是NEW -> INTERRUPTING -> INTERRUPTED。最後不管mayInterruptIfRunning為true還是false,此時都要調用finishCompletion方法喚醒阻塞的獲取異步任務結果的線程並移除線程等待鍊表節點。
從FutureTask.cancel源碼中我們可以得出答案,該方法並不能真正中斷正在執行異步任務的線程,只能對執行異步任務的線程發出中斷信號。如果執行異步任務的線程處於sleep、wait或join的狀態中,此時會拋出InterruptedException異常,該線程可以被中斷;此外,如果異步任務需要在while循環執行的話,此時可以結合以下代碼來結束異步任務線程,即執行異步任務的線程被中斷時,此時Thread.currentThread().isInterrupted()返回true,不滿足while循環條件因此退出循環,結束異步任務執行線程,如下代碼:
public Integer call() throws Exception {while (!Thread.currentThread().isInterrupted()) { // 業務邏輯代碼 System.out.println("running..."); }return 666;}注意:調用了FutureTask.cancel方法,只要返回結果是true,假如異步任務線程雖然不能被中斷,即使異步任務線程正常執行完畢,返回了執行結果,此時調用FutureTask.get方法也不能夠獲取異步任務執行結果,此時會拋出CancellationException異常。請問知道這是為什麼嗎?
因為調用了FutureTask.cancel方法,只要返回結果是true,此時的任務狀態為CANCELLED或INTERRUPTED,同時必然會執行finishCompletion方法,而finishCompletion方法會喚醒獲取異步任務結果的線程等待列表的線程,而獲取異步任務結果的線程喚醒後發現狀態s >= CANCELLED,此時就會拋出CancellationException異常了。
5 總結
好了,本篇文章對FutureTask的源碼分析就到此結束了,下面我們再總結下FutureTask的實現邏輯:
我們實現Callable接口,在覆寫的call方法中定義需要執行的業務邏輯;然後把我們實現的Callable接口實現對象傳給FutureTask,然後FutureTask作為異步任務提交給線程執行;最重要的是FutureTask內部維護了一個狀態state,任何操作(異步任務正常結束與否還是被取消)都是圍繞著這個狀態進行,並隨時更新state任務的狀態;只能有一個線程執行異步任務,當異步任務執行結束後,此時可能正常結束,異常結束或被取消。可以多個線程並發獲取異步任務執行結果,當異步任務還未執行完,此時獲取異步任務的線程將加入線程等待列表進行等待;當異步任務線程執行結束後,此時會喚醒獲取異步任務執行結果的線程,注意喚醒順序是"後進先出"即後面加入的阻塞線程先被喚醒。當我們調用FutureTask.cancel方法時並不能真正停止執行異步任務的線程,只是發出中斷線程的信號。但是只要cancel方法返回true,此時即使異步任務能正常執行完,此時我們調用get方法獲取結果時依然會拋出CancellationException異常。擴展:前面我們提到了FutureTask的runner,waiters和state都是用volatile關鍵字修飾,說明這三個變量都是多線程共享的對象(成員變量),會被多線程操作,此時用volatile關鍵字修飾是為了一個線程操作volatile屬性變量值後,能夠及時對其他線程可見。此時多線程操作成員變量僅僅用了volatile關鍵字仍然會有線程安全問題的,而此時Doug Lea老爺子沒有引入任何線程鎖,而是採用了Unsafe的CAS方法來代替鎖操作,確保線程安全性。
6 分析FutureTask源碼,我們能學到什麼?
我們分析源碼的目的是什麼?除了弄懂FutureTask的內部實現原理外,我們還要借鑑大佬寫寫框架源碼的各種技巧,只有這樣,我們才能成長。
分析了FutureTask源碼,我們可以從中學到:
利用LockSupport來實現線程的阻塞\喚醒機制;利用volatile和UNSAFE的CAS方法來實現線程共享變量的無鎖化操作;若要編寫超時異常的邏輯可以參考FutureTask的get(long timeout, TimeUnit unit)的實現邏輯;多線程獲取某一成員變量結果時若需要等待時的線程等待鍊表的邏輯實現;某一異步任務在某一時刻只能由單一線程執行的邏輯實現;FutureTask中的任務狀態state的變化處理的邏輯實現。以上列舉的幾點都是我們可以學以致用的地方。原創不易,若您覺得不錯,請無情的轉發和點讚吧!
【源碼筆記】Github地址:
https://github.com/yuanmabiji/Java-SourceCode-Blogs