Java的Future模式(基於JDK1.8)

1 概述

在我們使用多線程的時候,如果我們需要在主線程中拿到其餘線程執行輸出的內容,同時在子線程運行的時候,我們主線程依然運行,只是在合適的時候來拿子線程的結果就行了,這個時候我們怎麼辦呢?這就需要用到Future模式了。下面我們直接上一個列子來直觀地演示Future模式。

2 實例

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author: LIUTAO
 * @Date: Created in 2019/2/22  10:29
 * @Modified By:
 */
public class FutureDemo {
    public static void main(String[] args) {
        Callable<String> callable = () -> {

            //模擬烹飪食物的時間5秒
            Thread.sleep(5000);
            return "food is ok";
        };

        FutureTask<String> futureTask = new FutureTask(callable);
        new Thread(futureTask).start();
        System.out.println("begin to make cafe");
        try {

            //模擬煮咖啡的時間3秒
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("cafe is ok");
        if(!futureTask.isDone()){
            try {
                System.out.println("food is not ok , wait 3000 seconds");
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        try {
            String result = futureTask.get();
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

    }
}

運行上面的例子,如果我們再加入時間統計的代碼,我們可以看見整個過程消耗的時間遠遠小於煮咖啡和烹飪食物的事件總和,並且最終我們也拿到了烹飪的結果。接下來我們來分析一下Future模式的源碼。

3 源碼分析

(1)Callable接口

直接上源碼來看一下Callable接口的具體代碼。

@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;
}

Callable接口就只有一個call()函數,這個接口其實相當於Runnable接口的一個補充,這個接口的唯一函數改善了Runnable接口的唯一函數沒有返回值和無法處理異常的不足。

(2)FutureTask

FutureTask類其實是對Callable接口的進一步包裝,並且對外提供了很多操作函數,以此來獲取到線程的執行結果和執行的狀態。

我們首先還是來看一看FutureTask的繼承結構。

從上圖可以看出這個接口實現了RunnableFuture接口,其實RunnableFuture接口什麼也沒有做,僅僅將Runnable和Future接口進行了合併。這裏Future接口就規定了FutureTask類的主要函數。

get方法:獲取計算結果(如果還沒計算完,也是必須等待的)
cancel方法:還沒計算完,可以取消計算過程
isDone方法:判斷是否計算完
isCancelled方法:判斷計算是否被取消

針對FutureTask的函數分析,我們就僅僅分析下run()和get()函數,其餘的函數都比較簡單,大家可以自己看源碼。

A. run函數

public void run() {
        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 {

                    //獲取運行結果
                    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);
        }
    }

可以看出上面的邏輯還是非常簡單,將執行結果進行保存就行了。這裏涉及到一個state屬性,其實這個屬性就是對線程執行的狀態進行保存的,我們來看一看有哪些值。

    /**
     * 任務的運行狀態,初始時爲NEW。
     * 
     * 可能的狀態轉換:
     * 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;

B. get函數

通過get函數我們可以獲取Callable任務的執行結果,這裏我們來看一看get函數的具體邏輯。

public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }

當state的狀態爲NEW或者COMPLETING的時候將調用awaitDone函數,awaitDone函數根據傳入的參數來決定是否涉及到線程阻塞。

private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        final long deadline = timed ? System.nanoTime() + nanos : 0L;
        WaitNode q = null;
        boolean queued = false;
        for (;;) {

            //如果線程已經中斷,移除等待節點(這裏的等待節點就是由調用get的線程組成)
            if (Thread.interrupted()) {
                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)
                //初始化當前節點,從這可以看出調用get的所有線程將會組成一個由WaitNode組成的單向鏈表
                q = new WaitNode();
            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);
        }
    }

awaitDone函數的作用其實就是在Callable任務沒有執行完成前進行阻塞等待。那麼什麼時候會喚醒當代線程呢?這裏就是我們前面提到的set函數。

protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); 

            //處理完成狀態
            finishCompletion();
        }
    }

這裏的喚醒等待任務的操作其實是在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
    }

上面就是對Future模式的學習,歡迎交流和指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章