【併發程式設計】Future模式及JDK中的實現

weknow619發表於2019-02-22

本文講解Java中Future模式的使用,文章也釋出在了公號(點選檢視),歡迎交流。

1.1、Future模式是什麼

先簡單舉個例子介紹,當我們平時寫一個函式,函式裡的語句一行行同步執行,如果某一行執行很慢,程式就必須等待,直到執行結束才返回結果;但有時我們可能並不急著需要其中某行的執行結果,想讓被呼叫者立即返回。比如小明在某網站上成功建立了一個賬號,建立完賬號後會有郵件通知,如果在郵件通知時因某種原因耗時很久(此時賬號已成功建立),使用傳統同步執行的方式那就要等完這個時間才會有建立成功的結果返回到前端,但此時賬號建立成功後我們並不需要立即關心郵件傳送成功了沒,此時就可以使用Future模式,讓安在後臺慢慢處理這個請求,對於呼叫者來說,則可以先處理一些其他任務,在真正需要資料的場合(比如某時想要知道郵件傳送是否成功)再去嘗試獲取需要的資料。

使用Future模式,獲取資料的時候可能無法立即得到需要的資料。而是先拿到一個包裝,可以在需要的時候再去get獲取需要的資料。

1.2、Future模式與傳統模式的區別

先看看請求返回的時序圖,明顯傳統的模式是序列同步執行的,在遇到耗時操作的時候只能等待。反觀Future模式,發起一個耗時操作後,函式會立刻返回,並不會阻塞客戶端執行緒。所以在執行實際耗時操作時候客戶端無需等待,可以做其他事情,直到需要的時候再向工作執行緒獲取結果。

【併發程式設計】Future模式及JDK中的實現

2.1、動手實現簡易Future模式

下面的DataFuture類只是一個包裝類,建立它時無需阻塞等待。在工作執行緒準備好資料後使用setRealData方法將資料傳入。客戶端只要在真正需要資料時呼叫getRealData方法即可,如果此時資料已準備好則立即返回,否則getRealData方法就會等待,直到獲取資料完成。

public class DataFuture<T> {
    private T realData;
    private boolean isOK = false;

    public synchronized T getRealData() {
        while (!isOK) {
            try {
                // 資料未準備好則等待
                wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return realData;
    }
    
    public synchronized void setRealData(T data) {
        isOK = true;
        realData = data;
        notifyAll();
    }
}
複製程式碼

下面實現一服務端,客戶端向服務端請求資料時,服務端並不會立刻去載入真正資料,只是建立一個DataFuture,建立子執行緒去載入真正資料,服務端直接返回DataFuture即可。

public class Server {
    
    public DataFuture<String> getData() {
        final DataFuture<String> data = new DataFuture<>();
        Executors.newSingleThreadExecutor().execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                data.setRealData("最終資料");
            }
        });
        return data;
    }
}
複製程式碼

最終客戶端呼叫 程式碼如下:

long start = System.currentTimeMillis();
Server server = new Server();
DataFuture<String> dataFuture = server.getData();

try {
    // 先執行其他操作
    Thread.sleep(5000);
    // 模擬耗時...
} catch (InterruptedException e) {
    e.printStackTrace();
}

System.out.print("結果資料:" + dataFuture.getRealData());
System.out.println("耗時: " + (System.currentTimeMillis() - start));
複製程式碼

結果:

結果資料:最終資料
耗時: 5021
複製程式碼

執行最終資料耗時都在5秒左右,如果序列執行的話就是10秒左右。

2.2、JDK中的Future與FutureTask

先來看看Future介面原始碼:

public interface Future<V> {

    /**
     * 用來取消任務,取消成功則返回true,取消失敗則返回false。
     * mayInterruptIfRunning參數列示是否允許取消正在執行卻沒有執行完畢的任務,設為true,則表示可以取消正在執行過程中的任務。
     * 如果任務已完成,則無論mayInterruptIfRunning為true還是false,此方法都返回false,即如果取消已經完成的任務會返回false;
     * 如果任務正在執行,若mayInterruptIfRunning設定為true,則返回true,若mayInterruptIfRunning設定為false,則返回false;
     * 如果任務還沒有執行,則無論mayInterruptIfRunning為true還是false,肯定返回true。
     */
    boolean cancel(boolean mayInterruptIfRunning);

    /**
     * 表示任務是否被取消成功,如果在任務正常完成前被取消成功,則返回true
     */
    boolean isCancelled();

    /**
     * 表示任務是否已經完成,若任務完成,則返回true
     */
    boolean isDone();

    /**
     * 獲取執行結果,如果最終結果還沒得出該方法會產生阻塞,直到任務執行完畢返回結果
     */
    V get() throws InterruptedException, ExecutionException;

    /**
     * 獲取執行結果,如果在指定時間內,還沒獲取到結果,則丟擲TimeoutException
     */
    V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
複製程式碼

從上面原始碼可看出Future就是對於Runnable或Callable任務的執行進行查詢、中斷任務、獲取結果。下面就以一個計算1到1億的和為例子,看使用傳統方式和使用Future耗時差多少。先看傳統方式程式碼:

public class FutureTest {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        List<Integer> retList = new ArrayList<>();

        // 計算1000次1至1億的和
        for (int i = 0; i < 1000; i++) {
            retList.add(Calc.cal(100000000));
        }
        System.out.println("耗時: " + (System.currentTimeMillis() - start));

        for (int i = 0; i < 1000; i++) {
            try {
                Integer result = retList.get(i);
                System.out.println("第" + i + "個結果: " + result);
            } catch (Exception e) {
            }
        }
        System.out.println("耗時: " + (System.currentTimeMillis() - start));
    }

    public static class Calc implements Callable<Integer> {

        @Override
        public Integer call() throws Exception {
            return cal(10000);
        }

        public static int cal (int num) {
            int sum = 0;
            for (int i = 0; i < num; i++) {
                sum += i;
            }
            return sum;
        }
    }
}
複製程式碼

執行結果(耗時40+秒):

耗時: 436590個結果: 8874597121個結果: 8874597122個結果: 887459712
...
第999個結果: 887459712
耗時: 43688
複製程式碼

再來看看使用Future模式下程式:

public class FutureTest {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        ExecutorService executorService = Executors.newCachedThreadPool();
        List<Future<Integer>> futureList = new ArrayList<>();

        // 計算1000次1至1億的和
        for (int i = 0; i < 1000; i++) {
            // 排程執行
            futureList.add(executorService.submit(new Calc()));
        }
        System.out.println("耗時: " + (System.currentTimeMillis() - start));

        for (int i = 0; i < 1000; i++) {
            try {
                Integer result = futureList.get(i).get();
                System.out.println("第" + i + "個結果: " + result);
            } catch (InterruptedException | ExecutionException e) {
            }
        }
        System.out.println("耗時: " + (System.currentTimeMillis() - start));
    }

    public static class Calc implements Callable<Integer> {

        @Override
        public Integer call() throws Exception {
            return cal(100000000);
        }

        public static int cal (int num) {
            int sum = 0;
            for (int i = 0; i < num; i++) {
                sum += i;
            }
            return sum;
        }
    }
}
複製程式碼

執行結果(耗時12+秒):

耗時: 120580個結果: 8874597121個結果: 887459712
...
第999個結果: 887459712
耗時: 12405
複製程式碼

可以看到,計算1000次1至1億的和,使用Future模式併發執行最終的耗時比使用傳統的方式快了30秒左右,使用Future模式的效率大大提高。

2.3、FutureTask

說完Future,Future因為是介面不能直接用來建立物件,就有了下面的FutureTask。
先看看FutureTask的實現:

public class FutureTask<V> implements RunnableFuture<V>
複製程式碼

可以看到FutureTask類實現了RunnableFuture介面,接著看RunnableFuture介面原始碼:

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介面,也就是說其實FutureTask既可以作為Runnable被執行緒執行,也可以作為Future得到Callable的返回值。

看下面FutureTask的兩個構造方法,可以看出就是為這兩個操作準備的。

public FutureTask(Callable<V> var1) {
    if (var1 == null) {
        throw new NullPointerException();
    } else {
        this.callable = var1;
        this.state = 0;
    }
}

public FutureTask(Runnable var1, V var2) {
    this.callable = Executors.callable(var1, var2);
    this.state = 0;
}
複製程式碼

FutureTask使用例項:

public class FutureTest {

    public static void main(String[] args) {
        ExecutorService executor = Executors.newCachedThreadPool();
        Calc task = new Calc();
        FutureTask<Integer> futureTask = new FutureTask<Integer>(task);
        executor.submit(futureTask);
        executor.shutdown();
    }

    public static class Calc implements Callable<Integer> {

        @Override
        public Integer call() throws Exception {
            return cal(100000000);
        }

        public static int cal (int num) {
            int sum = 0;
            for (int i = 0; i < num; i++) {
                sum += i;
            }
            return sum;
        }
    }
}
複製程式碼

2.4、Future不足之處

上面例子可以看到使用Future模式比傳統模式效率明顯提高了,使用Future一定程度上可以讓一個執行緒池內的任務非同步執行;但同時也有個明顯的缺點:就是回撥無法放到與任務不同的執行緒中執行,傳統回撥最大的問題就是不能將控制流分離到不同的事件處理器中。比如主執行緒要等各個非同步執行執行緒返回的結果來做下一步操作,就必須阻塞在future.get()方法等待結果返回,這時其實又是同步了,如果遇到某個執行緒執行時間太長時,那情況就更糟了。

到Java8時引入了一個新的實現類CompletableFuture,彌補了上面的缺點,在下篇會講解CompletableFuture的使用。

作者注:原文發表在公號(點選檢視),定期分享IT網際網路、金融等工作經驗心得、人生感悟,歡迎訂閱交流,目前就職阿里-移動事業部,需要大廠內推的也可到公眾號砸簡歷,或檢視我個人資料獲取。(公號ID:weknow619)。

【併發程式設計】Future模式及JDK中的實現

相關文章