1. 程式人生 > >FutureTask簡單實戰

FutureTask簡單實戰

FutureTask是什麼?

執行緒池的實現核心之一是FutureTask。在提交任務時,使用者實現的Callable例項task會被包裝為FutureTask例項ftask;提交後任務非同步執行,無需使用者關心;當用戶需要時,再呼叫FutureTask#get()獲取結果——或異常。

基本使用

方法中可能會呼叫到多個服務/方法,且這些服務/方法之間是互相獨立的,不存在先後關係。在高併發場景下,如果執行比較耗時,可以考慮多執行緒非同步的方式呼叫。

我們先模擬兩個耗時服務

一個150ms,一個200ms

public class UserApi {

    /** 查詢使用者基本資訊,模擬耗時150ms */
    public String queryUserInfo(long userId) {
        String userInfo = "userInfo: " + userId;

        try {
            TimeUnit.MILLISECONDS.sleep(150L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return userInfo;
    }

    /** 查詢使用者地址,模擬耗時200ms */
    public String queryUserAddress(long userId) {
        String userAddress = "userAddress: " + userId;

        try {
            TimeUnit.MILLISECONDS.sleep(200L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return userAddress;
    }
}

不使用FutureTask

@Test
public void testNotUseFutureTask() {
    UserApi userApi = new UserApi();

    long userId = 12;
    long startTime = System.currentTimeMillis();

    // 獲取使用者基本資訊
    String userInfo = userApi.queryUserInfo(userId);
    // 獲取使用者地址
    String userAddress = userApi.queryUserAddress(userId);

    System.err.println("testNotUseFutureTask 耗時:" + (System.currentTimeMillis() - startTime));
}

執行幾次,結果:

testNotUseFutureTask 耗時:358
testNotUseFutureTask 耗時:360

從結果中,可以看到,總耗時是大於queryUserInfoqueryUserAddress之和的。但這兩個服務邏輯上並不存在先後關係,理論上最長耗時取決於最慢的那個,即queryUserAddress

使用FutureTask

下例使用了FutureTask,來非同步呼叫queryUserInfoqueryUserAddress

@Test
public void testUseFutureTask() throws ExecutionException, InterruptedException {
    UserApi userApi = new UserApi();

    long userId = 12;
    long startTime = System.currentTimeMillis();

    Callable<String> userInfoCallable = new Callable<String>() {
        @Override
        public String call() throws Exception {
            return userApi.queryUserInfo(userId);
        }
    };
    Callable<String> userAddressCallable = new Callable<String>() {
        @Override
        public String call() throws Exception {
            return userApi.queryUserAddress(userId);
        }
    };
    FutureTask<String> userInfoFutureTask = new FutureTask<>(userInfoCallable);
    FutureTask<String> userAddressFutureTask = new FutureTask<>(userAddressCallable);

    new Thread(userInfoFutureTask).start();
    new Thread(userAddressFutureTask).start();

    String userInfo = userInfoFutureTask.get();
    String userAddress = userAddressFutureTask.get();
    System.err.println("testUseFutureTask 耗時:" + (System.currentTimeMillis() - startTime));
}

執行幾次,結果:

testUseFutureTask 耗時:239
testUseFutureTask 耗時:237

很明顯,總耗時大大減少了,這就驗證了前面所說,總耗時取決於queryUserAddress的耗時。

實現一個簡單的FutureTask

從前面使用FutureTask的程式碼中可以看到,一個FutureTask需要包含以下幾點:

1、範型
2、建構函式,傳入Callable
3、實現Runnable
4、有返回值
  • MyFutureTask程式碼如下:
public class MyFutureTask<T> implements Runnable {

    private Callable<T> callable;
    private T           result;
    private String      state;

    public MyFutureTask(Callable<T> callable) {
        this.callable = callable;
    }

    @Override
    public void run() {
        state = "NEW";
        try {
            result = callable.call();
        } catch (Exception e) {
            e.printStackTrace();
        }
        state = "DONE";
        synchronized (this) {
            this.notify();
        }
    }

    /** 獲取呼叫結果 */
    public T get() throws InterruptedException {
        if ("DOEN".equals(state)) {
            return result;
        }
        synchronized (this) {
            this.wait();
        }
        return result;
    }
}
  • 使用:
@Test
public void testMyUseFutureTask() throws InterruptedException {
    UserApi userApi = new UserApi();

    long userId = 12;
    long startTime = System.currentTimeMillis();

    Callable<String> userInfoCallable = new Callable<String>() {
        @Override
        public String call() throws Exception {
            return userApi.queryUserInfo(userId);
        }
    };
    Callable<String> userAddressCallable = new Callable<String>() {
        @Override
        public String call() throws Exception {
            return userApi.queryUserAddress(userId);
        }
    };
    
    // 不同點
    MyFutureTask<String> userInfoFutureTask = new MyFutureTask<>(userInfoCallable);
    MyFutureTask<String> userAddressFutureTask = new MyFutureTask<>(userAddressCallable);

    new Thread(userInfoFutureTask).start();
    new Thread(userAddressFutureTask).start();

    String userInfo = userInfoFutureTask.get();
    String userAddress = userAddressFutureTask.get();
    System.err.println("testMyUseFutureTask 耗時:" + (System.currentTimeMillis() - startTime));
}
  • 輸出結果:
testMyUseFutureTask 耗時:208
testMyUseFutureTask 耗時:211

從結果中看到,預期與使用FutureTask的一致。至於使用我們自定義的MyFutureTask執行耗時為何會比FutureTask長,我猜測是我們自己寫的未做更多的檢查和判斷。我們自己寫的只是用來學習FutureTask

總結

不使用非同步的方式時,queryUserAddressqueryUserInfo執行之後才會執行,兩者相加的時間算入總呼叫耗時。如果使用了非同步執行緒呼叫,由於queryUserAddress耗時長,這樣在queryUserAddress執行結束前,queryUserInfo就執行結束了,這樣queryUserInfo呼叫耗時就不計了。