Java多執行緒開發 - Future的使用與理解
著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。
Future模式是什麼
Future模式是多執行緒開發中非常常見的一種設計模式。它的核心思想是非同步呼叫。當我們需要呼叫一個函式方法時。如果這個函式執行很慢,那麼我們就要進行等待。但有時候,我們可能並不急著要結果。因此,我們可以讓被呼叫者立即返回,讓他在後臺慢慢處理這個請求。對於呼叫者來說,則可以先處理一些其他任務,在真正需要資料的場合再去嘗試獲取需要的資料。
用生活中的例子來打個比喻,就像叫外賣。比如在午休之前我們可以提前叫外賣,只需要點好食物,下個單。然後我們可以繼續工作。到了中午下班的時候外賣也就到了,然後就可以吃個午餐,再美滋滋的睡個午覺。而如果你在下班的時候才叫外賣,那就只能坐在那裡乾等著外賣小哥,最後拿到外賣吃完午飯,午休時間也差不多結束了。
使用Future模式,獲取資料的時候無法立即得到需要的資料。而是先拿到一個契約,你可以在將來需要的時候再用這個契約去獲取需要的資料,這個契約就好比叫外賣的例子裡的外賣訂單。
普通方式和Future模式的區別
我們可以看一下使用普通模式和用Future模式的時序圖。可以看出來普通模式是序列的,在遇到耗時操作的時候只能等待。而Future模式,只是發起了耗時操作,函式立馬就返回了,並不會阻塞客戶端執行緒。所以在工作執行緒執行耗時操作的時候客戶端無需等待,可以繼續做其他事情,等到需要的時候再向工作執行緒獲取結果:
Future模式的簡單實現
首先是FutureData,它只是一個包裝類,建立它不需要耗時。在工作執行緒準備好資料之後可以使用setData
getData
方法即可,如果這個時候資料還沒有準備好,那麼getData
方法就會等待,如果已經準備好了就好直接返回。
public class FutureData<T> {
private boolean mIsReady = false;
private T mData;
public synchronized void setData(T data) {
mIsReady = true;
mData = data;
notifyAll();
}
public synchronized T getData () {
while (!mIsReady) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return mData;
}
}
複製程式碼
接著是服務端,客戶端在向服務端請求資料的時候服務端不會實際去載入資料,它只是建立一個FutureData,然後建立子執行緒去載入,而它只需要直接返回FutureData就可以了。
public class Server {
public FutureData<String> getString() {
final FutureData<String> data = new FutureData<>();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data.setData("world");
}
}).start();
return data;
}
}
複製程式碼
客戶端程式碼如下,整個程式只需要執行2秒多,但如果不使用Future模式的話就需要三秒了。
Server server = new Server();
FutureData<String> futureData = server.getString();
//先執行其他操作
String hello = "hello";
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.print(hello + " " + futureData.getData());
複製程式碼
JDK中的Future模式
public interface Executor {
void execute(Runnable command);
}
public interface ExecutorService extends Executor {
...
}
複製程式碼
execute
方法其實是在Executor
中定義的,而ExecutorService
繼承了Executor
。它只是簡單的提交了一個Runnable
給執行緒池中的執行緒去呼叫:
public interface ExecutorService extends Executor {
...
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task,T result);
Future<?> submit(Runnable task);
...
}
複製程式碼
而submit方法是ExecutorService中定義的,它們都會返回一個Future物件。實際上submit方法就是使用的Future模式:
- Future<?> submit(Runnable task) :
它的返回值實際上是Future,子執行緒是不會返回資料的。 - Future submit(Runnable task,T result) :
這個方法是不是很蛋疼,返回的結果在呼叫的時候已經給出了。如果我一開始就知道結果那我為什麼又要發起子執行緒呢?
其實不然,這個result可以是一個代理,它不是實際的結果,它只是儲存了結果。我這裡給出一個例子大家體會一下吧:
final String[] result = new String[1];
Runnable r = new Runnable() {
public void run() {
result[0] = "hello world";
}
};
Future<String[]> future = Executors.newSingleThreadExecutor().submit(r,result);
try {
System.out.println("result[0]: " + future.get()[0]);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
結果:result[0]: hello world
複製程式碼
- Future submit(Callable task)
這個方法就比較好理解了,Callable.call()方法在子執行緒中被呼叫,同時它有返回值,只要將載入的資料直接return出來就好:
Future<String> future = Executors.newSingleThreadExecutor()
.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "Hello World";
}
});
try {
System.out.print(future.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
結果:Hello World
複製程式碼
實戰演示
比如我們在計算兩個List中的數的總和的時候就可以用Future模式提高效率:
public int getTotal() throws ExecutionException,InterruptedException {
ArrayList<Integer> a = ListUtil.newArrayList(1,1);
ArrayList<Integer> b = ListUtil.newArrayList(1,1,1);
Future<Integer> future = Executors.newCachedThreadPool().submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int r = 0;
for (int num : a) {
r += num;
}
return r;
}
});
int r = 0;
for (int num : b) {
r += num;
}
return r + future.get();
}
結果:5
複製程式碼