揭開Future的神祕面紗——任務取消

貓毛·波拿巴發表於2018-09-04

系列目錄:

使用案例

在之前寫過的一篇隨筆中已經提到了Future的應用場景和特性。(ExecutorService——<T> Future<T> submit(Callable<T> task)

我們先來回顧一下:

public class FutureCancelDemo {


    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool();
        Future<Target> future = exec.submit(new DemoTask());
        TimeUnit.SECONDS.sleep(2); //給足時間讓啟動起來,但又不足以讓其完成
        future.cancel(true);
    }

}

class Target { //任務目標

}

class DemoTask implements Callable<Target> { //任務
    private static int counter = 0;
    private final int id = counter++;

    @Override
    public Target call() throws Exception {
        System.out.println(this+ " start...");
        TimeUnit.SECONDS.sleep(5); //模擬任務執行需要的時間
        System.out.println(this + " completed!");
        return new Target();
    }

    @Override
    public String toString() {
        return "Task[" + id + "]";
    }
}

 

一般情況下,我們會在哪裡用到Future物件呢?

  就是當我們需要控制任務(Runnable/Callable物件)的時候,我們把任務提交給執行器(ExecutorService.submit()),並返回一個控制控制程式碼(Future)。以便在未來的某個時刻檢查任務執行狀態、獲取任務執行結果、以及在必要的時候取消任務等。

今天我們就來看看,Future是如何取消任務的。

 任務取消做了什麼

我們知道Future只是一個介面,它到底是如何實現任務取消的呢?

  我們知道,把任務提交給執行器,執行器返回給我們一個Future。但是由於程式碼封裝得很好,Future和ExecutorService都只是一個介面,我們只知道怎麼用,卻不知道其內部是如何實現的。如果要檢視它到底是如何實現的就要追根溯源的查,直到找到最終的實現類。首先,我們的ExecutorService是用工廠類Executors獲得的。就拿上述程式碼為例,我們獲得了一個緩衝執行緒池ThreadPoolExecutor型別的物件,而ThreadPoolExecutor並沒有重寫submit方法,而是延用它父類的實現,而它父類便是AbstractExecutorSevice。這個類提供了ExecutorService介面最基本的底層實現。我們終於找到了submit方法的實現。

  

從這裡我們可以看到,該方法以RunnableFuture作為返回值。且該值由newTaskFor方法生成。

  

然而這RunnableFuture,FutureTask,Future到底是何關係呢?

  

即RunnableFuture繼承了Runnable及Future介面,表示一個可控制的任務。而FutureTask實現了這個介面。故Future的取消操作最終由這個FutureTask實現。

我們來看看它是如何實現取消操作(future.cancel())的。

   

由上我們知道,取消操作與一個變數mayInterruptIfRunning有很大關聯。由以上程式碼可知,

mayInterruptIfRunning為true時

  (1)設定任務狀態為INTERRUPTING

  (2)中斷執行任務的執行緒

  (3)若中斷成功,設定任務狀態為INTERRUPTED

  (4)執行finishCompletion() => 此方法與等待執行緒有關。請看結果獲取

mayInterruptIfRunning為false時

  (1)設定任務狀態為CANCELLED

  (2)執行finishCompletion() 同上

我們來看看cancel方法的定義。

  

說的就是,如果任務已經開始被執行緒執行,那麼mayInterruptIfRunning決定的是,在任務執行過程中,要不要通過中斷其執行執行緒來嘗試打斷任務。

任務取消≠任務不再執行

熟悉執行緒中斷的都知道,它是一種協作機制,如果任務程式碼中沒有對中斷訊號進行響應,那麼程式就會繼續執行下去。這是中斷的情況,不中斷自然肯定不能打斷任務的執行。

那麼問題就來了,這個取消操作到底有什麼意義呢?

意義就是,如果任務已被標識為已取消,那麼獲取其結果的執行緒,就不需要等待其完成,丟擲已取消異常。事實上任務其實還在跑。

 

通過一個例子加深一下印象:

public class FutureCancelDemo2 {


    public static void main(String[] args) throws InterruptedException {
        ExecutorService exec = Executors.newCachedThreadPool(); //緩衝執行緒池

        Future<Target> future = exec.submit(new DemoTask());
        TimeUnit.SECONDS.sleep(2); //給足時間讓啟動起來,但又不足以讓其完成
        boolean cancelResult1 = future.cancel(true); //true表示,如果已經執行,則中斷

        Future<Target> future2 = exec.submit(new DemoTask());
        TimeUnit.SECONDS.sleep(2);
        boolean cancelResult2 = future2.cancel(false);

        System.out.println("cancelResult1:" + cancelResult1);
        System.out.println("cancelResult2:" + cancelResult2);

        try {
            Target target = future2.get();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

}

執行結果:

很明顯,程式中,在任務2(Task[1])在其執行完成之前,已經被我們"取消"了。但是從控制檯輸出可知,它其實還在跑,並在完成的時候輸出"Task[1] completed!"。然後我們嘗試獲取任務2(Task[1])的結果,發現其丟擲了已取消異常。

cancel方法返回值的含義

一切都跟前面符合前面的說明,但是細心的朋友可能發現一件奇怪的事情。那就是為什麼不管是cancel(false)還是cancel(true)返回值都是true?

我們來看看官方的定義:

也就是說,cancel的返回值如果是false,則情況有這幾種:在此操作之前,任務已經完成,在這之前已經呼叫過一次cancel,其他原因導致無法取消。

其他任何情況都會返回true

 

相關文章