java多執行緒系列之future機制

recklessMo發表於2017-10-13

java多執行緒系列之future機制

future是什麼?

  • 在執行比較耗時的任務的時候,我們經常會採取新開執行緒執行的方式,比如在netty中,如果在io執行緒中處理耗cpu的計算任務,那麼就會造成io執行緒的堵塞,導致吞吐率的下降(比較好理解,本來io執行緒可以去處理io的,現在卻在等待cpu執行計算任務),這嚴重影響了io的效率。
  • 一般我們採用執行緒池來執行非同步任務,一般情況下不需要獲取返回值,但是特殊情況下是需要獲取返回值的,也就是需要拿到非同步任務的執行結果,舉個例子來說:對大量整數進行求和,如果採用多執行緒來求解,就需要一個彙匯流排程和多個計算執行緒,計算執行緒執行具體的計算任務並且返回求和值,彙匯流排程進行多個求和值最後的彙總。
  • 那麼如果我們自己要實現這個非同步計算的程式的話可以採用什麼方式呢?這實際上是執行緒之間的通訊機制,即我們的彙匯流排程需要拿到所有計算執行緒執行完畢的結果,那麼我們可以採用共享記憶體來實現,定義一個全域性的map,每個計算執行緒執行完畢的結果都放到到map中,然後彙匯流排程從全域性map中取出結果進行累加彙總,這樣就搞定了,這裡面雖然思想很簡單,但是還是有一些細節需要考慮的,比如彙匯流排程怎麼判斷所有的任務都執行完畢呢?可以通過計算任務的總數和已經完成計算任務的數目進行比較。總之我們肯定可以實現一套這樣的非同步計算框架。
  • 那麼進一步抽象,在上面的實現過程中,實際上我們關心的就是每個任務執行的結果,以及任務是否執行完畢,對應到上面提到的計算框架,就是我們關心是否計算完畢和計算完畢後的值,有了這兩部分的值,我們的彙匯流排程就能夠很方便的進行計算總的結果了。
  • 其實仔細觀察,對於幾乎所有的非同步執行執行緒,我們都是關心這兩部分值的,即任務是否執行完畢,執行完後的結果(如果不需要結果可以返回null),那麼這兩部分的東西肯定可以抽象出來,避免我們每次編寫執行緒執行的run方法的時候都要自己提交結果和設定完成標誌,於是java就是設計了這麼一套future機制來幫助開發者

上面就是我結合自己的理解分析的future機制的設計思想,可能說的不夠全,希望有人可以補充。下面會講解java future的具體實現

總結一句話:我們需要非同步執行任務並且知道非同步任務的執行結果和執行狀態,我們可以自己來實現,但是由於這部分比較通用,所以java通過一種future機制來為我們實現了這些功能,這就是future。

下面分析java裡面future機制的具體實現

  • execute方式:我們知道一個類如果實現了runnable介面,它就能夠被執行緒來執行,因為實現了runnable介面就擁有了run方法,所以能夠被執行。所以最簡單的非同步執行緒執行方式如下:利用Executors框架來建立一個執行緒池,然後呼叫execute方法來提交非同步任務,注意這裡的execute方法是沒有返回的,也就是說我們沒法知道提交的任務的執行結果。

    ExecutorService executorService = Executors.newSingleThreadExecutor();
    executorService.execute(()->System.out.println("非同步執行!"));複製程式碼
  • submit方式:前面提到的java給我們提供的執行緒池介面ExecutorService提供了兩種提交非同步任務的方式,一種就是沒有返回值的execute方法(由於ExecutorService介面是extends了Executor介面的,所以擁有了execute方法),還有一種是帶有返回值的submit方法。在submit方法中,提供了三個過載方法:

    <T> Future<T> submit(Callable<T> task);
        Future<?> submit(Runnable task);
    <T> Future<T> submit(Runnable task, T result);複製程式碼

    可以看到,submit方法支援實現了callable和runnable的task,不同於runnable只有沒有返回值的run方法,callable提供了一個帶返回值的call方法,可以有返回值。正是因為runnable沒有返回值,所以第二個過載方法返回值為null,第三個過載方法裡面可以從外部設定一個返回值,這個返回值將會作為runnable的返回值。具體程式碼如下:

        public <T> Future<T> submit(Callable<T> task) {
            if (task == null) throw new NullPointerException();
            RunnableFuture<T> ftask = newTaskFor(task);
            execute(ftask);
            return ftask;
        }
        public Future<?> submit(Runnable task) {
            if (task == null) throw new NullPointerException();
            RunnableFuture<Void> ftask = newTaskFor(task, null);
            execute(ftask);
            return ftask;
        }
        public <T> Future<T> submit(Runnable task, T result) {
            if (task == null) throw new NullPointerException();
            RunnableFuture<T> ftask = newTaskFor(task, result);
            execute(ftask);
            return ftask;
        }複製程式碼

    兩個方法都呼叫newTaskFor方法來建立了一個RunnableFuture的物件,然後呼叫execute方法來執行這個物件,說明我們執行緒池真正執行的物件就是這個RunnableFuture物件。

        protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
            return new FutureTask<T>(runnable, value);
        }
        protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
            return new FutureTask<T>(callable);
        }複製程式碼

    由上面程式碼看出就是建立了一個futureTask物件,這個物件封裝了我們提供的runnable和callable物件。futuretask實現了runnablefuture介面,這就是說明futuretask具備了runnable的功能(能被執行緒執行)和future功能(能夠獲取自身執行的結果和狀態)。能被執行緒執行功能是我們自己通過實現runnable介面或者callable介面來完成的。future功能前面我們提過是很通用的功能,所以java給我們實現了。下面就進入futuretask檢視。

  • futuretask物件:futuretask是真正的future功能實現的地方。前面說過這個一個RunnableFuture物件,所以我們看看它的run方法

        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;    
        /** 封裝的callable物件 */
        private Callable<V> callable;
        /** task的執行結果 */
        private Object outcome; 
        /** 當前執行緒池的哪個執行緒正在執行這個task */
        private volatile Thread runner;
        /** 等待的執行緒列表 */
        private volatile WaitNode waiters;
    
        public void run() {
            if (state != NEW ||
                !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                             null, Thread.currentThread()))
                return;
            try {
                Callable<V> c = callable;// 1. 內部包裝的一個callable物件
                if (c != null && state == NEW) {
                    V result;
                    boolean ran;
                    try {
                        result = c.call();// 2. 呼叫包裝的call方法
                        ran = true;
                    } catch (Throwable ex) {
                        result = null;
                        ran = false;
                        setException(ex);
                    }
                    if (ran)
                        set(result);//3. 設定返回值
                }
            } 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);
            }
        }複製程式碼

    前面提到futuretask是封裝了runnable和callable的,可是為什麼內部只有一個callable呢,實際上是因為futuretask自己呼叫介面卡轉換了一下:程式碼如下,採用了java的介面卡模式。

        public FutureTask(Runnable runnable, V result) {
            this.callable = Executors.callable(runnable, result);
            this.state = NEW;       // ensure visibility of callable
        }
    
        public static <T> Callable<T> callable(Runnable task, T result) {
            if (task == null)
                throw new NullPointerException();
            return new RunnableAdapter<T>(task, result);
        }
    
        static final class RunnableAdapter<T> implements Callable<T> {
            final Runnable task;
            final T result;
            RunnableAdapter(Runnable task, T result) {
                this.task = task;
                this.result = result;
            }
            public T call() {
                task.run();
                return result;
            }
        }複製程式碼

    futuretask的run方法呼叫了內部封裝的callable物件的call方法,獲取返回值,並且設定到自己outcome中,state代表執行的狀態,這樣就通過代理的方式代理了我們的callable的call方法,幫助我們獲取執行的結果和狀態,所以我們自己編寫業務邏輯的時候就不用去管這層通用的邏輯了。這裡面還有一個waitnode我們單獨講

  • WaitNode: 通過前面的分析我們知道,實際上我們submit任務之後返回的future物件就是執行緒池為我們建立的runnablefuture物件,也就是futuretask這個物件。future介面為我們提供了一系列的方法,如下

        V get() throws InterruptedException, ExecutionException;
        boolean cancel(boolean mayInterruptIfRunning);複製程式碼

    上面是主要的兩個方法,get和cancel,cancel的時候呼叫runner的interrupt方法即可

        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
                        UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
                    }
                }
            } finally {
                finishCompletion();
            }
            return true;
        }複製程式碼

    其中unsafe是用於cas操作的,在java併發包中大量用到,後續會講解。

    get方法的設計是阻塞的,也就是說如果結果沒有返回時需要等待的,所以才會有waitnode這個物件的產生,當多個執行緒都呼叫futuretask的get方法的時候,如果結果還沒產生,就都需要等待,這時候所有等待的執行緒就會形成一個連結串列,所以waitnode實際上就是執行緒的連結串列。

        static final class WaitNode {
            volatile Thread thread;
            volatile WaitNode next;
            WaitNode() { thread = Thread.currentThread(); }
        }複製程式碼

    再看get方法:如果任務沒有完成就呼叫awaitDone進入阻塞,如果完成了直接呼叫report返回結果

        public V get() throws InterruptedException, ExecutionException {
            int s = state;
            if (s <= COMPLETING)
                s = awaitDone(false, 0L);
            return report(s);
        }複製程式碼
        private int awaitDone(boolean timed, long nanos)
            throws InterruptedException {
            final long deadline = timed ? System.nanoTime() + nanos : 0L;
            WaitNode q = null;
            boolean queued = false;
            for (;;) {
                if (Thread.interrupted()) {//1. 如果等待過程中,被中斷過了,那麼就移除自己
                    removeWaiter(q);
                    throw new InterruptedException();
                }
    
                int s = state;
                if (s > COMPLETING) {
                    if (q != null)
                        q.thread = null;
                    return s;
                }
                else if (s == COMPLETING) // cannot time out yet
                    Thread.yield();
                else if (q == null)
                    q = new WaitNode();
                else if (!queued)//2. cas更新連結串列節點
                    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);//3. locksupport原語讓執行緒進入休眠
                }
                else
                    LockSupport.park(this);
            }
        }複製程式碼

    還是比較好看懂,其中LockSupport是原語,讓執行緒進行休眠。如果執行緒在休眠中醒來了,有可能是多種情況,比如get的時間到了,也就是從3中醒來了,這樣的話下一次迴圈就會判斷時間到了,從而remove掉節點退出。還有可能等待的執行緒被interrupt了,這時候就會走到1的邏輯,通過判斷中斷標記將其remove掉。

    既然有了waitnode這個等待連結串列,那麼肯定會有相應的喚醒機制,當執行完畢之後就會將waitnode連結串列上的執行緒一次喚醒,如下。

        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
        }複製程式碼

實際上java的future介面所提供的功能比較有限,比如listen機制就沒有,都需要非同步任務發起者主動去查詢狀態和結果,而且沒有提供非阻塞的等待機制。但是我們可以自己靈活的實現,後續將參照netty中的future機制進行詳細講解。

相關文章