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
從結果中,可以看到,總耗時是大於queryUserInfo
和queryUserAddress
之和的。但這兩個服務邏輯上並不存在先後關係,理論上最長耗時取決於最慢的那個,即queryUserAddress
使用FutureTask
下例使用了FutureTask
,來非同步呼叫queryUserInfo
和queryUserAddress
。
@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
。
總結
不使用非同步的方式時,queryUserAddress
在queryUserInfo
執行之後才會執行,兩者相加的時間算入總呼叫耗時。如果使用了非同步執行緒呼叫,由於queryUserAddress
耗時長,這樣在queryUserAddress
執行結束前,queryUserInfo
就執行結束了,這樣queryUserInfo
呼叫耗時就不計了。