不會用Java Future,我懷疑你泡茶沒我快, 又是超長圖文!!

日拱一兵發表於2020-07-10

mountains-1412683_1280.png

  • 你有一個思想,我有一個思想,我們交換後,一個人就有兩個思想
  • If you can NOT explain it simply, you do NOT understand it well enough

前言

建立執行緒有幾種方式?這個問題的答案應該是可以脫口而出的吧

  • 繼承 Thread 類
  • 實現 Runnable 介面

但這兩種方式建立的執行緒是屬於”三wu產品“:

  • 沒有引數
  • 沒有返回值
  • 沒辦法丟擲異常
class MyThread implements Runnable{
   @Override
   public void run() {
      log.info("my thread");
   }
}

Runnable 介面是 JDK1.0 的核心產物

 /**
 * @since   JDK1.0
 */
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

用著 “三wu產品” 總是有一些弊端,其中沒辦法拿到返回值是最讓人不能忍的,於是 Callable 就誕生了

Callable

又是 Doug Lea 大師,又是 Java 1.5 這個神奇的版本

 /**
 * @see Executor
 * @since 1.5
 * @author Doug Lea
 * @param <V> the result type of method {@code call}
 */
@FunctionalInterface
public interface Callable<V> {
    
    V call() throws Exception;
}

Callable 是一個泛型介面,裡面只有一個 call() 方法,該方法可以返回泛型值 V ,使用起來就像這樣:

Callable<String> callable = () -> {
    // Perform some computation
    Thread.sleep(2000);
    return "Return some result";
};

二者都是函式式介面,裡面都僅有一個方法,使用上又是如此相似,除了有無返回值,Runnable 與 Callable 就點差別嗎?

Runnable VS Callable

兩個介面都是用於多執行緒執行任務的,但他們還是有很明顯的差別的

執行機制

先從執行機制上來看,Runnable 你太清楚了,它既可以用在 Thread 類中,也可以用在 ExecutorService 類中配合執行緒池的使用;Bu~~~~t, Callable 只能在 ExecutorService 中使用,你翻遍 Thread 類,也找不到Callable 的身影

異常處理

Runnable 介面中的 run 方法簽名上沒有 throws ,自然也就沒辦法向上傳播受檢異常;而 Callable 的 call() 方法簽名卻有 throws,所以它可以處理受檢異常;

所以歸納起來看主要有這幾處不同點:

整體差別雖然不大,但是這點差別,卻具有重大意義

返回值和處理異常很好理解,另外,在實際工作中,我們通常要使用執行緒池來管理執行緒(原因已經在 為什麼要使用執行緒池? 中明確說明),所以我們就來看看 ExecutorService 中是如何使用二者的

ExecutorService

先來看一下 ExecutorService 類圖

我將上圖示記的方法單獨放在此處

void execute(Runnable command);

<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);

可以看到,使用ExecutorService 的 execute() 方法依舊得不到返回值,而 submit() 方法清一色的返回 Future 型別的返回值

細心的朋友可能已經發現, submit() 方法已經在 CountDownLatch 和 CyclicBarrier 傻傻的分不清楚? 文章中多次使用了,只不過我們沒有獲取其返回值罷了,那麼

  • Future 到底是什麼呢?
  • 怎麼通過它獲取返回值呢?

我們帶著這些疑問一點點來看

Future

Future 又是一個介面,裡面只有五個方法:

從方法名稱上相信你已經能看出這些方法的作用

// 取消任務
boolean cancel(boolean mayInterruptIfRunning);

// 獲取任務執行結果
V get() throws InterruptedException, ExecutionException;

// 獲取任務執行結果,帶有超時時間限制
V get(long timeout, TimeUnit unit) throws InterruptedException,                             ExecutionException,  TimeoutException;

// 判斷任務是否已經取消
boolean isCancelled();

// 判斷任務是否已經結束
boolean isDone();

鋪墊了這麼多,看到這你也許有些亂了,我們們趕緊看一個例子,演示一下幾個方法的作用

@Slf4j
public class FutureAndCallableExample {

   public static void main(String[] args) throws InterruptedException, ExecutionException {
      ExecutorService executorService = Executors.newSingleThreadExecutor();

      // 使用 Callable ,可以獲取返回值
      Callable<String> callable = () -> {
         log.info("進入 Callable 的 call 方法");
         // 模擬子執行緒任務,在此睡眠 2s,
         // 小細節:由於 call 方法會丟擲 Exception,這裡不用像使用 Runnable 的run 方法那樣 try/catch 了
         Thread.sleep(5000);
         return "Hello from Callable";
      };

      log.info("提交 Callable 到執行緒池");
      Future<String> future = executorService.submit(callable);

      log.info("主執行緒繼續執行");

      log.info("主執行緒等待獲取 Future 結果");
      // Future.get() blocks until the result is available
      String result = future.get();
      log.info("主執行緒獲取到 Future 結果: {}", result);

      executorService.shutdown();
   }
}

程式執行結果如下:

如果你執行上述示例程式碼,主執行緒呼叫 future.get() 方法會阻塞自己,直到子任務完成。我們也可以使用 Future 方法提供的 isDone 方法,它可以用來檢查 task 是否已經完成了,我們將上面程式做點小修改:

// 如果子執行緒沒有結束,則睡眠 1s 重新檢查
while(!future.isDone()) {
   System.out.println("Task is still not done...");
   Thread.sleep(1000);
}

來看執行結果:

如果子程式執行時間過長,或者其他原因,我們想 cancel 子程式的執行,則我們可以使用 Future 提供的 cancel 方法,繼續對程式做一些修改

while(!future.isDone()) {
   System.out.println("子執行緒任務還沒有結束...");
   Thread.sleep(1000);

   double elapsedTimeInSec = (System.nanoTime() - startTime)/1000000000.0;

      // 如果程式執行時間大於 1s,則取消子執行緒的執行
   if(elapsedTimeInSec > 1) {
      future.cancel(true);
   }
}

來看執行結果:

為什麼呼叫 cancel 方法程式會出現 CancellationException 呢? 是因為呼叫 get() 方法時,明確說明了:

呼叫 get() 方法時,如果計算結果被取消了,則丟擲 CancellationException (具體原因,你會在下面的原始碼分析中看到)

有異常不處理是非常不專業的,所以我們需要進一步修改程式,以更友好的方式處理異常

// 通過 isCancelled 方法判斷程式是否被取消,如果被取消,則列印日誌,如果沒被取消,則正常呼叫 get() 方法
if (!future.isCancelled()){
   log.info("子執行緒任務已完成");
   String result = future.get();
   log.info("主執行緒獲取到 Future 結果: {}", result);
}else {
   log.warn("子執行緒任務被取消");
}

檢視程式執行結果:

相信到這裡你已經對 Future 的幾個方法有了基本的使用印象,但 Future 是介面,其實使用 ExecutorService.submit() 方法返回的一直都是 Future 的實現類 FutureTask

接下來我們就進入這個核心實現類一探究竟

FutureTask

同樣先來看類結構

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

很神奇的一個介面,FutureTask 實現了 RunnableFuture 介面,而 RunnableFuture 介面又分別實現了 RunnableFuture 介面,所以可以推斷出 FutureTask 具有這兩種介面的特性:

  • Runnable 特性,所以可以用在 ExecutorService 中配合執行緒池使用
  • Future 特性,所以可以從中獲取到執行結果

FutureTask原始碼分析

如果你完整的看過 AQS 相關分析的文章,你也許會發現,閱讀 Java 併發工具類原始碼,我們無非就是要關注以下這三點:

- 狀態 (程式碼邏輯的主要控制)
- 佇列 (等待排隊佇列)
- CAS (安全的set 值)


腦海中牢記這三點,我們們開始看 FutureTask 原始碼,看一下它是如何圍繞這三點實現相應的邏輯的

文章開頭已經提到,實現 Runnable 介面形式建立的執行緒並不能獲取到返回值,而實現 Callable 的才可以,所以 FutureTask 想要獲取返回值,必定是和 Callable 有聯絡的,這個推斷一點都沒錯,從構造方法中就可以看出來:

public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

即便在 FutureTask 構造方法中傳入的是 Runnable 形式的執行緒,該構造方法也會通過 Executors.callable 工廠方法將其轉換為 Callable 型別:

public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

但是 FutureTask 實現的是 Runnable 介面,也就是隻能重寫 run() 方法,run() 方法又沒有返回值,那問題來了:

  • FutureTask 是怎樣在 run() 方法中獲取返回值的?
  • 它將返回值放到哪裡了?
  • get() 方法又是怎樣拿到這個返回值的呢?

我們來看一下 run() 方法(關鍵程式碼都已標記註釋)

public void run() {
      // 如果狀態不是 NEW,說明任務已經執行過或者已經被取消,直接返回
      // 如果狀態是 NEW,則嘗試把執行執行緒儲存在 runnerOffset(runner欄位),如果賦值失敗,則直接返回
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
          // 獲取建構函式傳入的 Callable 值
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                  // 正常呼叫 Callable 的 call 方法就可以獲取到返回值
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                  // 儲存 call 方法丟擲的異常
                setException(ex);
            }
            if (ran)
                  // 儲存 call 方法的執行結果
                set(result);
        }
    } finally {        
        runner = null;       
        int s = state;
          // 如果任務被中斷,則執行中斷處理
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

run() 方法沒有返回值,至於 run() 方法是如何將 call() 方法的返回結果和異常都儲存起來的呢?其實非常簡單, 就是通過 set(result) 儲存正常程式執行結果,或通過 setException(ex) 儲存程式異常資訊

/** The result to return or exception to throw from get() */
private Object outcome; // non-volatile, protected by state reads/writes

// 儲存異常結果
protected void setException(Throwable t) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = t;
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
        finishCompletion();
    }
}

// 儲存正常結果
protected void set(V v) {
  if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
    outcome = v;
    UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
    finishCompletion();
  }
}

setExceptionset 方法非常相似,都是將異常或者結果儲存在 Object 型別的 outcome 變數中,outcome 是成員變數,就要考慮執行緒安全,所以他們要通過 CAS方式設定 outcome 變數的值,既然是在 CAS 成功後 更改 outcome 的值,這也就是 outcome 沒有被 volatile 修飾的原因所在。

儲存正常結果值(set方法)與儲存異常結果值(setException方法)兩個方法程式碼邏輯,唯一的不同就是 CAS 傳入的 state 不同。我們上面提到,state 多數用於控制程式碼邏輯,FutureTask 也是這樣,所以要搞清程式碼邏輯,我們需要先對 state 的狀態變化有所瞭解

 /*
 *
 * Possible state transitions:
 * NEW -> COMPLETING -> NORMAL  //執行過程順利完成
 * NEW -> COMPLETING -> EXCEPTIONAL //執行過程出現異常
 * NEW -> CANCELLED // 執行過程中被取消
 * NEW -> INTERRUPTING -> INTERRUPTED //執行過程中,執行緒被中斷
 */
private volatile int state;
private static final int NEW          = 0;
private static final int COMPLETING   = 1;
private static final int NORMAL       = 2;
private static final int EXCEPTIONAL  = 3;
private static final int CANCELLED    = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED  = 6;

7種狀態,千萬別慌,整個狀態流轉其實只有四種線路

FutureTask 物件被建立出來,state 的狀態就是 NEW 狀態,從上面的建構函式中你應該已經發現了,四個最終狀態 NORMAL ,EXCEPTIONAL , CANCELLED , INTERRUPTED 也都很好理解,兩個中間狀態稍稍有點讓人困惑:

  • COMPLETING: outcome 正在被set 值的時候
  • INTERRUPTING:通過 cancel(true) 方法正在中斷執行緒的時候

總的來說,這兩個中間狀態都表示一種瞬時狀態,我們將幾種狀態圖形化展示一下:

我們知道了 run() 方法是如何儲存結果的,以及知道了將正常結果/異常結果儲存到了 outcome 變數裡,那就需要看一下 FutureTask 是如何通過 get() 方法獲取結果的:

public V get() throws InterruptedException, ExecutionException {
    int s = state;
      // 如果 state 還沒到 set outcome 結果的時候,則呼叫 awaitDone() 方法阻塞自己
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
      // 返回結果
    return report(s);
}

awaitDone 方法是 FutureTask 最核心的一個方法

// get 方法支援超時限制,如果沒有傳入超時時間,則接受的引數是 false 和 0L
// 有等待就會有佇列排隊或者可響應中斷,從方法簽名上看有 InterruptedException,說明該方法這是可以被中斷的
private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
      // 計算等待截止時間
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
          // 如果當前執行緒被中斷,如果是,則在等待對立中刪除該節點,並丟擲 InterruptedException
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }

        int s = state;
          // 狀態大於 COMPLETING 說明已經達到某個最終狀態(正常結束/異常結束/取消)
          // 把 thread 只為空,並返回結果
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
          // 如果是COMPLETING 狀態(中間狀態),表示任務已結束,但 outcome 賦值還沒結束,這時主動讓出執行權,讓其他執行緒優先執行(只是發出這個訊號,至於是否別的執行緒執行一定會執行可是不一定的)
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
          // 等待節點為空
        else if (q == null)
              // 將當前執行緒構造節點
            q = new WaitNode();
          // 如果還沒有入佇列,則把當前節點加入waiters首節點並替換原來waiters
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                 q.next = waiters, q);
          // 如果設定超時時間
        else if (timed) {
            nanos = deadline - System.nanoTime();
              // 時間到,則不再等待結果
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
              // 阻塞等待特定時間
            LockSupport.parkNanos(this, nanos);
        }
        else
              // 掛起當前執行緒,知道被其他執行緒喚醒
            LockSupport.park(this);
    }
}

總的來說,進入這個方法,通常會經歷三輪迴圈

  1. 第一輪for迴圈,執行的邏輯是 q == null, 這時候會新建一個節點 q, 第一輪迴圈結束。
  2. 第二輪for迴圈,執行的邏輯是 !queue,這個時候會把第一輪迴圈中生成的節點的 next 指標指向waiters,然後CAS的把節點q 替換waiters, 也就是把新生成的節點新增到waiters 中的首節點。如果替換成功,queued=true。第二輪迴圈結束。
  3. 第三輪for迴圈,進行阻塞等待。要麼阻塞特定時間,要麼一直阻塞知道被其他執行緒喚醒。

對於第二輪迴圈,大家可能稍稍有點迷糊,我們前面說過,有阻塞,就會排隊,有排隊自然就有佇列,FutureTask 內部同樣維護了一個佇列

/** Treiber stack of waiting threads */
private volatile WaitNode waiters;

說是等待佇列,其實就是一個 Treiber 型別 stack,既然是 stack, 那就像手槍的彈夾一樣(腦補一下子彈放入彈夾的情形),後進先出,所以剛剛說的第二輪迴圈,會把新生成的節點新增到 waiters stack 的首節點

如果程式執行正常,通常呼叫 get() 方法,會將當前執行緒掛起,那誰來喚醒呢?自然是 run() 方法執行完會喚醒,設定返回結果(set方法)/異常的方法(setException方法) 兩個方法中都會呼叫 finishCompletion 方法,該方法就會喚醒等待佇列中的執行緒

private void finishCompletion() {
    // assert state > COMPLETING;
    for (WaitNode q; (q = waiters) != null;) {
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            for (;;) {
                Thread t = q.thread;
                if (t != null) {
                    q.thread = null;
                      // 喚醒等待佇列中的執行緒
                    LockSupport.unpark(t);
                }
                WaitNode next = q.next;
                if (next == null)
                    break;
                q.next = null; // unlink to help gc
                q = next;
            }
            break;
        }
    }

    done();

    callable = null;        // to reduce footprint
}

將一個任務的狀態設定成終止態只有三種方法:

  • set
  • setException
  • cancel

前兩種方法已經分析完,接下來我們就看一下 cancel 方法

檢視 Future cancel(),該方法註釋上明確說明三種 cancel 操作一定失敗的情形

  1. 任務已經執行完成了
  2. 任務已經被取消過了
  3. 任務因為某種原因不能被取消

其它情況下,cancel操作將返回true。值得注意的是,cancel操作返回 true 並不代表任務真的就是被取消, 這取決於發動cancel狀態時,任務所處的狀態

  • 如果發起cancel時任務還沒有開始執行,則隨後任務就不會被執行;
  • 如果發起cancel時任務已經在執行了,則這時就需要看 mayInterruptIfRunning 引數了:

    • 如果mayInterruptIfRunning 為true, 則當前在執行的任務會被中斷
    • 如果mayInterruptIfRunning 為false, 則可以允許正在執行的任務繼續執行,直到它執行完

有了這些鋪墊,看一下 cancel 程式碼的邏輯就秒懂了

public boolean cancel(boolean mayInterruptIfRunning) {
  
    if (!(state == NEW &&
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
              mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {    // in case call to interrupt throws exception
          // 需要中斷任務執行執行緒
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                  // 中斷執行緒
                if (t != null)
                    t.interrupt();
            } finally { // final state
                  // 修改為最終狀態 INTERRUPTED
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
          // 喚醒等待中的執行緒
        finishCompletion();
    }
    return true;
}

核心方法終於分析完了,到這我們們喝口茶休息一下吧

我是想說,使用 FutureTask 來演練燒水泡茶經典程式

如上圖:

  • 洗水壺 1 分鐘
  • 燒開水 15 分鐘
  • 洗茶壺 1 分鐘
  • 洗茶杯 1 分鐘
  • 拿茶葉 2 分鐘

最終泡茶

讓我心算一下,如果序列總共需要 20 分鐘,但很顯然在燒開水期間,我們可以洗茶壺/洗茶杯/拿茶葉

這樣總共需要 16 分鐘,節約了 4分鐘時間,燒水泡茶尚且如此,在現在高併發的時代,4分鐘可以做的事太多了,學會使用 Future 優化程式是必然(其實優化程式就是尋找關鍵路徑,關鍵路徑找到了,非關鍵路徑的任務通常就可以和關鍵路徑的內容並行執行了

@Slf4j
public class MakeTeaExample {

   public static void main(String[] args) throws ExecutionException, InterruptedException {
      ExecutorService executorService = Executors.newFixedThreadPool(2);

      // 建立執行緒1的FutureTask
      FutureTask<String> ft1 = new FutureTask<String>(new T1Task());
      // 建立執行緒2的FutureTask
      FutureTask<String> ft2 = new FutureTask<String>(new T2Task());

      executorService.submit(ft1);
      executorService.submit(ft2);

      log.info(ft1.get() + ft2.get());
      log.info("開始泡茶");

      executorService.shutdown();
   }

   static class T1Task implements Callable<String> {

      @Override
      public String call() throws Exception {
         log.info("T1:洗水壺...");
         TimeUnit.SECONDS.sleep(1);

         log.info("T1:燒開水...");
         TimeUnit.SECONDS.sleep(15);

         return "T1:開水已備好";
      }
   }

   static class T2Task implements Callable<String> {
      @Override
      public String call() throws Exception {
         log.info("T2:洗茶壺...");
         TimeUnit.SECONDS.sleep(1);

         log.info("T2:洗茶杯...");
         TimeUnit.SECONDS.sleep(2);

         log.info("T2:拿茶葉...");
         TimeUnit.SECONDS.sleep(1);
         return "T2:福鼎白茶拿到了";
      }
   }
}

上面的程式是主執行緒等待兩個 FutureTask 的執行結果,執行緒1 燒開水時間更長,執行緒1希望在水燒開的那一剎那就可以拿到茶葉直接泡茶,怎麼半呢?

那隻需要線上程 1 的FutureTask 中獲取 執行緒 2 FutureTask 的返回結果就可以了,我們稍稍修改一下程式:

@Slf4j
public class MakeTeaExample1 {

   public static void main(String[] args) throws ExecutionException, InterruptedException {
      ExecutorService executorService = Executors.newFixedThreadPool(2);

      // 建立執行緒2的FutureTask
      FutureTask<String> ft2 = new FutureTask<String>(new T2Task());
      // 建立執行緒1的FutureTask
      FutureTask<String> ft1 = new FutureTask<String>(new T1Task(ft2));
      
      executorService.submit(ft1);
      executorService.submit(ft2);

      executorService.shutdown();
   }

   static class T1Task implements Callable<String> {

      private FutureTask<String> ft2;
      public T1Task(FutureTask<String> ft2) {
         this.ft2 = ft2;
      }

      @Override
      public String call() throws Exception {
         log.info("T1:洗水壺...");
         TimeUnit.SECONDS.sleep(1);

         log.info("T1:燒開水...");
         TimeUnit.SECONDS.sleep(15);

         String t2Result = ft2.get();
         log.info("T1 拿到T2的 {}, 開始泡茶", t2Result);
         return "T1: 上茶!!!";
      }
   }

   static class T2Task implements Callable<String> {
      @Override
      public String call() throws Exception {
         log.info("T2:洗茶壺...");
         TimeUnit.SECONDS.sleep(1);

         log.info("T2:洗茶杯...");
         TimeUnit.SECONDS.sleep(2);

         log.info("T2:拿茶葉...");
         TimeUnit.SECONDS.sleep(1);
         return "福鼎白茶";
      }
   }
}

來看程式執行結果:

知道這個變化後我們再回頭看 ExecutorService 的三個 submit 方法:

<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
<T> Future<T> submit(Callable<T> task);

第一種方法,逐層程式碼檢視到這裡:

你會發現,和我們改造燒水泡茶的程式思維是相似的,可以傳進去一個 result,result 相當於主執行緒和子執行緒之間的橋樑,通過它主子執行緒可以共享資料

第二個方法引數是 Runnable 型別引數,即便呼叫 get() 方法也是返回 null,所以僅是可以用來斷言任務已經結束了,類似 Thread.join()

第三個方法引數是 Callable 型別引數,通過get() 方法可以明確獲取 call() 方法的返回值

到這裡,關於 Future 的整塊講解就結束了,還是需要簡單消化一下的

總結

如果熟悉 Javascript 的朋友,Future 的特性和 Javascript 的 Promise 是類似的,私下開玩笑通常將其比喻成男朋友的承諾

迴歸到Java,我們從 JDK 的演變歷史,談及 Callable 的誕生,它彌補了 Runnable 沒有返回值的空缺,通過簡單的 demo 瞭解 Callable 與 Future 的使用。 FutureTask 又是 Future介面的核心實現類,通過閱讀原始碼瞭解了整個實現邏輯,最後結合FutureTask 和執行緒池演示燒水泡茶程式,相信到這裡,你已經可以輕鬆獲取執行緒結果了

燒水泡茶是非常簡單的,如果更復雜業務邏輯,以這種方式使用 Future 必定會帶來很大的會亂(程式結束沒辦法主動通知,Future 的連結和整合都需要手動操作)為了解決這個短板,沒錯,又是那個男人 Doug Lea, CompletableFuture 工具類在 Java1.8 的版本出現了,搭配 Lambda 的使用,讓我們編寫非同步程式也像寫序列程式碼那樣簡單,縱享絲滑

接下來我們就瞭解一下 CompletableFuture 的使用

靈魂追問

  1. 你在日常開發工作中是怎樣將整塊任務做到分工與協作的呢?有什麼基本準則嗎?
  2. 如何批量的執行非同步任務呢?

參考

  1. Java 併發程式設計實戰
  2. Java 併發程式設計的藝術
  3. Java 併發程式設計之美

日拱一兵 | 原創

相關文章