主頁 > 後端開發 > 不會用Java Future,我懷疑你泡茶沒我快, 又是超長圖文!!

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

2020-10-02 04:40:41 後端開發

  • 你有一個思想,我有一個思想,我們交換后,一個人就有兩個思想

  • If you can NOT explain it simply, you do NOT understand it well enough

現陸續將Demo代碼和技術文章整理在一起 Github實踐精選 ,方便大家閱讀查看,本文同樣收錄在此,覺得不錯,還請Star

前言

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

  • 繼承 Thread 類
  • 實作 Runnable 介面

但這兩種方式創建的執行緒是屬于”三無產品“:

  • 沒有引數
  • 沒有回傳值
  • 沒辦法拋出例外
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();
}

用著 “三無產品” 總是有一些弊端,其中沒辦法拿到回傳值是最讓人不能忍的,于是 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 并發編程之美

個人博客:https://dayarch.top
加我微信好友, 進群娛樂學習交流,備注「進群」

歡迎持續關注公眾號:「日拱一兵」

  • 前沿 Java 技術干貨分享
  • 高效工具匯總 | 回復「工具」
  • 面試問題分析與解答
  • 技術資料領取 | 回復「資料」

以讀偵探小說思維輕松趣味學習 Java 技術堆疊相關知識,本著將復雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......


轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/147247.html

標籤:Java

上一篇:在一個大型的c++專案里面,專案 ,模塊,源檔案,源程式 ,主函式到底是什么關系?

下一篇:微服務之間最佳呼叫方式是什么?

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more