Java中 FutureTask 的使用

在這裏插入圖片描述在這裏插入圖片描述

在前面 通過Callable和Future創建線程 已經學習了 Callable 和 Future 兩個接口,以及 FutureTask 的簡單使用,節約篇幅,這裏就不重複介紹了。

介紹

我覺得 FutureTask 其實就是實實在在的工具類,我們把具體的任務詳情在 Callable 接口的實現類中實現,然後將實現類的實例傳給 FutureTask,讓他來創建任務,它還需要調度者來調度執行。

FutureTask 的使用很簡單,通過源碼可以看出,FutureTask實現了Runnable和Future接口。
因爲 FutureTask 實現了 Runnable 接口,所以可以將 FutureTask 對象作爲任務提交給 ThreadPoolExecutor 去執行,也可以直接被 hread 執行。
又因爲實現了Future接口,所以也能用來獲得任務的執行結果。

下面直接使用 FutureTask 完成一個有趣的程序

示例

我在學習《Java併發編程實戰》的時候,看到一個叫做“燒水泡茶”程序。下面就用 FutureTask 來模擬一下這個工序。

燒水泡茶需要下面這些工序:洗水壺,燒開水,洗茶壺,洗茶杯,拿茶葉,泡茶。他們每一步耗時不同。

併發編程可以總結爲三個核心問題:分工、同步和互斥。

對於燒水泡茶這個程序,一種最優的分工方案可以是下圖所示的這樣: 用兩個線程T1和T2來完成燒水泡茶程序

  • T1負責洗水壺、燒開水、泡茶這三道工序。
  • T2負責洗茶壺、洗茶杯、拿茶葉三道工序,其中T1在執行泡茶這道工序時需要等待T2完成拿茶葉的工序。

在這裏插入圖片描述

首先,我們創建了兩個FutureTask——FutureTask_1 和FutureTask_2,FutureTask_1 完成洗水壺、燒開水、泡茶的任務,FutureTask_2 完成洗茶壺、洗茶杯、拿茶葉的任務。

這裏需要注意的是 FutureTask_1 這個任務在執行泡茶任務前,需要等待 FutureTask_2 把茶葉拿來,所以 FutureTask_1 內部需要引用 FutureTask_2,並在執行泡茶之前,我們可以充分利用 Future 的 get 方法的阻塞性質來調用 FutureTask_2 的get()方法實現等待。

FutureTask_1 需要執行的任務:洗水壺、燒開水、泡茶:

/**
 * FutureTask_1 需要執行的任務:洗水壺、燒開水、泡茶
 */
public class FutureTask_1 implements Callable<String> {

    // FutureTask_1 中持有 FutureTask_2 的引用
    FutureTask<String> futureTask_2;

    // 通過構造器初始化 成員變量
    public FutureTask_1(FutureTask<String> futureTask_2) {
        this.futureTask_2 = futureTask_2;
    }

    // 重寫的 Callable 接口中的 call 方法。
    @Override
    public String call() throws Exception {

        System.out.println("T1:洗水壺");
        TimeUnit.SECONDS.sleep(1);

        System.out.println("T1:燒開水");
        TimeUnit.SECONDS.sleep(15);

        // 獲取 T2 線程的茶葉
        String teas = futureTask_2.get();
        System.out.println("拿到茶葉:" + teas);

        System.out.println("T1:開始泡茶...");

        return "上茶:" + teas;
    }
}

FutureTask_2 需要執行的任務:洗茶壺、洗茶杯、拿茶葉:

/**
 * FutureTask_2 需要執行的任務:洗茶壺、洗茶杯、拿茶葉
 */
public class FutureTask_2 implements Callable<String> {

    @Override
    public String call() throws Exception {

        System.out.println("T2:洗茶壺");
        TimeUnit.SECONDS.sleep(1);

        System.out.println("T2:洗茶杯");
        TimeUnit.SECONDS.sleep(2);

        System.out.println("T2:拿茶葉");
        TimeUnit.SECONDS.sleep(1);

        return "峨眉雪尖兒";
    }
}

測試方法:

    @Test
    public void makeTea(){

        // 創建 FutureTask_2 的任務
        FutureTask<String> futureTask_2 = new FutureTask<>(new FutureTask_2());
        // 創建 FutureTask_1 的任務,並將 FutureTask_2 任務的引用傳入
        FutureTask<String> futureTask_1 = new FutureTask<>(new FutureTask_1(futureTask_2));

        // 創建線程 T1,來執行任務 FutureTask_1
        Thread t1 = new Thread(futureTask_1);
        t1.start();

        // 創建線程 T2,來執行任務 FutureTask_2
        Thread t2 = new Thread(futureTask_2);
        t2.start();

        try {
            // 獲取任務 FutureTask_1 的最後一步的結果
            System.out.println(futureTask_1.get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

執行結果:

T2:洗茶壺
T1:洗水壺
T1:燒開水
T2:洗茶杯
T2:拿茶葉
拿到茶葉:峨眉雪尖兒
T1:開始泡茶...
上茶:峨眉雪尖兒

如果多執行幾次會發現,在拿到茶葉前的執行結果有可能都不不一樣的,這就說明,我們 T1 和 T2 兩個線程在並行執行。

利用多線程可以快速將一些串行的任務並行化,從而提高性能。
如果任務之間有依賴關係,比如當前任務依賴前一個任務的執行結果,這種問題基本上都可以用 Future 來解決。在分析這種問題的過程中,建議先用圖描述一下任務之間的依賴關係,同時將線程的分工也做好,類似於燒水泡茶最優分工方案那幅圖。對照圖來寫代碼,好處是更形象,且不易出錯。


技 術 無 他, 唯 有 熟 爾。
知 其 然, 也 知 其 所 以 然。
踏 實 一 些, 不 要 着 急, 你 想 要 的 歲 月 都 會 給 你。


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