Callable/Future 使用及原理分析,Future .get()為啥能等待呢?

線程池的執行任務有兩種方法,一種是 submit、一種是 execute;這兩個方法是有區別的,那么基于這個區別我們再來看看。

execute 和 submit 區別

  1. execute 只可以接收一個 Runnable 的參數

  2. execute 如果出現異常會拋出

  3. execute 沒有返回值

  4. submit 可以接收 Runable 和 Callable 這兩種類型的參數,

  5. 對于 submit 方法,如果傳入一個 Callable,可以得到一個 Future 的返回值

  6. submit 方法調用不會拋異常,除非調用 Future.get
    這里,我們重點了解一下 Callable/Future,可能很多同學知道他是一個帶返回值的線程,但是具體的實現可能不清楚。

Callable/Future 案例演示

Callable/Future 和 Thread 之類的線程構建最大的區別在于,能夠很方便的獲取線程執行完以后的結果。首先來看一個簡單的例子

public class CallableDemo implements Callable<String> {
    public String call() throws Exception {
        Thread.sleep(3000);//阻塞案例演示
        return "hello world";
    }

    public static void main(String[] args) throws ExecutionException,
            InterruptedException {
        CallableDemo callableDemo = new CallableDemo();
        FutureTask futureTask = new FutureTask(callableDemo);
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
    }
}

想一想我們為什么需要使用回調呢?那是因為結果值是由另一線程計算的,當前線程是不知道結果值什么時候計算完成,所以它傳遞一個回調接口給計算線程,當計算完成時,調用這個回調接口,回傳結果值。

這個在很多地方有用到,比如 Dubbo 的異步調用,比如消息中間件的異步通信等等… 利用 FutureTask、 Callable、 Thread 對耗時任務(如查詢數據庫)做預處理,在需要計算結果之前就啟動計算。

所以我們來看一下 Future/Callable 是如何實現的

Callable/Future 原理分析

在剛剛實現的 demo 中,我們用到了兩個 api,分別是 Callable 和 FutureTask。Callable 是一個函數式接口,里面就只有一個 call 方法。子類可以重寫這個方法,并且這個方法會有一個返回值

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

FutureTask

FutureTask 的類關系圖如下,它實現 RunnableFuture 接口,那么這個 RunnableFuture 接口的作用是什么呢。

在講解 FutureTask 之前,先看看 Callable, Future, FutureTask 它們之間的關系圖,如下:

public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
  void run();
}

RunnableFuture 是一個接口,它繼承了 Runnable 和 Future 這兩個接口, Runnable 太熟悉了, 那么 Future 是什么呢?

Future 表示一個任務的生命周期,并提供了相應的方法來判斷是否已經完成或取消,以及獲取任務的結果和取消任務等。

public interface Future<V> {
  boolean cancel(boolean mayInterruptIfRunning);
  // 當前的 Future 是否被取消,返回 true 表示已取消
  boolean isCancelled();
  // 當前 Future 是否已結束。包括運行完成、拋出異常以及取消,都表示當前 Future 已結束
  boolean isDone();
  // 獲取 Future 的結果值。如果當前 Future 還沒有結束,那么當前線程就等待,
  // 直到 Future 運行結束,那么會喚醒等待結果值的線程的。
  V get() throws InterruptedException, ExecutionException;
  // 獲取 Future 的結果值。與 get()相比較多了允許設置超時時間
  V get(long timeout, TimeUnit unit)
  throws InterruptedException, ExecutionException, TimeoutException;
}

分析到這里我們其實有一些初步的頭緒了, FutureTask 是 Runnable 和 Future 的結合,如果我們把 Runnable 比作是生產者, Future 比作是消費者,那么 FutureTask 是被這兩者共享的,生產者運行 run 方法計算結果,消費者通過 get 方法獲取結果。

作為生產者消費者模式,有一個很重要的機制,就是如果生產者數據還沒準備的時候,消費者會被阻塞。當生產者數據準備好了以后會喚醒消費者繼續執行。

這個有點像我們上次可分析的阻塞隊列,那么在 FutureTask 里面是基于什么方式實現的呢?

state 的含義

表示 FutureTask 當前的狀態,分為七種狀態

private static final int NEW = 0; // NEW 新建狀態,表示這個 FutureTask還沒有開始運行
// COMPLETING 完成狀態, 表示 FutureTask 任務已經計算完畢了
// 但是還有一些后續操作,例如喚醒等待線程操作,還沒有完成。
private static final int COMPLETING = 1;
// FutureTask 任務完結,正常完成,沒有發生異常
private static final int NORMAL = 2;
// FutureTask 任務完結,因為發生異常。
private static final int EXCEPTIONAL = 3;
// FutureTask 任務完結,因為取消任務
private static final int CANCELLED = 4;
// FutureTask 任務完結,也是取消任務,不過發起了中斷運行任務線程的中斷請求
private static final int INTERRUPTING = 5;
// FutureTask 任務完結,也是取消任務,已經完成了中斷運行任務線程的中斷請求
private static final int INTERRUPTED = 6;

run 方法

   public void run() {
        // 如果狀態 state 不是 NEW,或者設置 runner 值失敗
        // 表示有別的線程在此之前調用 run 方法,并成功設置了 runner 值
        // 保證了只有一個線程可以運行 try 代碼塊中的代碼。
        if (state != NEW ||
                !UNSAFE.compareAndSwapObject(this, runnerOffset,
                        null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            //只有c不為null且狀態state為NEW的情況
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    //調用callable的call方法,并獲得返回結果
                    result = c.call();
                    //運行成功
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    //設置結果
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

其實 run 方法作用非常簡單,就是調用 callable 的 call 方法返回結果值 result,根據是否發生異常,調用 set(result)或 setException(ex)方法表示 FutureTask 任務完結。

不過因為 FutureTask 任務都是在多線程環境中使用,所以要注意并發沖突問題。注意在 run方法中,我們沒有使用 synchronized 代碼塊或者 Lock 來解決并發問題,而是使用了 CAS 這個樂觀鎖來實現并發安全,保證只有一個線程能運行 FutureTask 任務。

get 方法

get 方法就是阻塞獲取線程執行結果,這里主要做了兩個事情

  1. 判斷當前的狀態,如果狀態小于等于 COMPLETING,表示 FutureTask 任務還沒有完結,所以調用 awaitDone 方法,讓當前線程等待。
  2. report 返回結果值或者拋出異常
    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }

awaitDone

如果當前的結果還沒有被執行完,把當前線程線程和插入到等待隊列

    private int awaitDone(boolean timed, long nanos)
            throws InterruptedException {
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        FutureTask.WaitNode q = null;
        boolean queued = false;// 節點是否已添加
        for (;;) {
            // 如果當前線程中斷標志位是 true,
            // 那么從列表中移除節點 q,并拋出 InterruptedException 異常
            if (Thread.interrupted()) {
                removeWaiter(q);
                throw new InterruptedException();
            }

            int s = state;
            // 當狀態大于 COMPLETING 時,表示FutureTask任務已結束
            if (s > COMPLETING) {
                if (q != null)
                    // 將節點 q 線程設置為 null,因為線程沒有阻塞等待
                    q.thread = null;
                return s;
            }
            // 表示還有一些后序操作沒有完成,那么當前線程讓出執行權
            else if (s == COMPLETING) // cannot time out yet
                Thread.yield();
                //表示狀態是 NEW,那么就需要將當前線程阻塞等待。
                // 就是將它插入等待線程鏈表中
            else if (q == null)
                q = new FutureTask.WaitNode();
            // 使用 CAS 函數將新節點添加到鏈表中,如果添加失敗,那么queued 為 false,
            // 下次循環時,會繼續添加,知道成功。
            else if (!queued)
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                        q.next = waiters, q);
            // timed 為 true 表示需要設置超時
            else if (timed) {
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    removeWaiter(q);
                    return state;
                }
                // 讓當前線程等待 nanos 時間
                LockSupport.parkNanos(this, nanos);
            }
            else
                LockSupport.park(this);
        }
    }

被阻塞的線程,會等到 run 方法執行結束之后被喚醒

report

report 方法就是根據傳入的狀態值 s,來決定是拋出異常,還是返回結果值。 這個兩種情況都表示 FutureTask 完結了

private V report(int s) throws ExecutionException {
    Object x = outcome;//表示 call 的返回值
    if (s == NORMAL) // 表示正常完結狀態,所以返回結果值
        return (V)x;
    // 大于或等于 CANCELLED,都表示手動取消 FutureTask 任務,
    // 所以拋出 CancellationException 異常
  if (s >= CANCELLED)
    throw new CancellationException();
   // 否則就是運行過程中,發生了異常,這里就拋出這個異常
   throw new ExecutionException((Throwable)x);
}

線程池對于 Future/Callable 的執行

我們現在再來看線程池里面的 submit 方法,就會很清楚了。

public class CallableDemo implements Callable<String> {
    public String call() throws Exception {
        Thread.sleep(3000);//阻塞案例演示
        return "hello world";
    }

    public static void main(String[] args) throws ExecutionException,
            InterruptedException {
        ExecutorService es= Executors.newFixedThreadPool(1);
        CallableDemo callableDemo = new CallableDemo();
        FutureTask futureTask = new FutureTask(callableDemo);

        Future future=es.submit(callableDemo);
        System.out.println(futureTask.get());
    }
}

AbstractExecutorService.submit

調用抽象類中的 submit 方法,這里其實相對于 execute 方法來說,只多做了一步操作,就是封裝了一個 RunnableFuture

   public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

newTaskFor

更簡單

 protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }

然后調用 execute 方法,這里面的邏輯前面分析過了,會通過 worker 線程來調用過 ftask 的run 方法。而這個 ftask 其實就是 FutureTask 里面最終實現的邏輯。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。