怎麼用wait、notify巧妙的設計一個Future模式?
我們知道多執行緒可以實現同時執行多個任務(只是看起來是同時,其實是CPU的時間片切換特別快我們沒感覺而已)。
現在假設一個做飯的場景,你沒有廚具也沒有食材。你可以去網上買一個廚具,但是這段時間,你不需要閒著啊,可以同時去超市買食材。
設想這是兩個執行緒,主執行緒去買食材,然後開啟一個子執行緒去買廚具。但是,子執行緒是需要返回一個廚具的。 如果用普通的執行緒,只有一個Run方法,而Run方法是沒有返回值的,這個時候該怎麼辦呢?
我們就可以用JDK提供的Future模式。在主執行緒買完食材之後,可以主動去獲取子執行緒的廚具。(本文認為讀者瞭解Future,因此不對Future用法做過多介紹)
程式碼如下:
public class FutureCook { static class Chuju { } static class Shicai{ } public static void cook(Chuju chuju,Shicai shicai){ System.out.println("最後:烹飪中..."); } public static void main(String[] args) throws InterruptedException, ExecutionException { //第一步,網購廚具 Callable<Chuju> shopping = new Callable<Chuju>(){ @Override public Chuju call() throws Exception { System.out.println("第一步:下單"); System.out.println("第一步:等待送貨"); Thread.sleep(5000); //模擬送貨時間 System.out.println("第一步:快遞送到"); return new Chuju(); } }; FutureTask<Chuju> task = new FutureTask<Chuju>(shopping); new Thread(task).start(); //第二步,購買食材 Thread.sleep(2000); Shicai shicai = new Shicai(); System.out.println("第二步:食材到位"); //第三步,烹飪 if(!task.isDone()){ //是否廚具到位 System.out.println("第三步:廚具還沒到,請等待,也可以取消"); //① // task.cancel(true); // System.out.println("已取消"); // return; } //嘗試獲取結果,如果獲取不到,就會進入等待狀態 // 即main執行緒等待子執行緒執行結束才能繼續往下執行 Chuju chuju = task.get(); System.out.println("第三步:廚具到位,可以烹飪了"); cook(chuju,shicai); } }
返回結果:
第一步:下單
第一步:等待送貨
第二步:食材到位
第三步:廚具還沒到,請等待,也可以取消
第一步:快遞送到
第三步:廚具到位,可以烹飪了
最後:烹飪中...
以上程式碼表示,子執行緒購買廚具消耗的時間比較長(假定5秒),而主執行緒購買食材比較快(2秒),所以我在第三步烹飪之前,先去判斷一下買廚具的執行緒是否執行完畢。此處肯定返回false,然後主執行緒可以選擇繼續等待,也可以選擇取消。(把①註釋開啟即可測試取消)
我們可以看到,利用Future模式,可以把原本同步執行的任務改為非同步執行,可以充分利用CPU資源,提高效率。
現在,我用wait、notify的方式來實現和以上Future模式一模一樣的效果。
大概思想就是,建立一個FutureClient端去發起請求,通過FutureData先立即返回一個結果(此時相當於只返回一個請求成功的通知),然後再去開啟一個執行緒非同步地執行任務,獲取真實資料RealData。此時,主執行緒可以繼續執行其他任務,當需要資料的時候,就可以呼叫get方法拿到真實資料。
1)定義一個數據介面,包含獲取資料的get方法,判斷任務是否執行完畢的isDone方法,和取消任務的cancel方法。
public interface Data<T> {
T get();
boolean isDone();
boolean cancel();
}
2)定義真實資料的類,實現Data介面,用來執行實際的任務和返回真實資料。
public class RealData<T> implements Data<T>{
private T result ;
public RealData (){
this.prepare();
}
private void prepare() {
//準備資料階段,只有準備完成之後才可以繼續往下走
try {
System.out.println("第一步:下單");
System.out.println("第一步:等待送貨");
Thread.sleep(5000);
System.out.println("第一步:快遞送到");
} catch (InterruptedException e) {
System.out.println("被中斷:"+e);
//重新設定中斷狀態
Thread.currentThread().interrupt();
}
Main.Chuju chuju = new Main.Chuju();
result = (T)chuju;
}
@Override
public T get() {
return result;
}
@Override
public boolean isDone() {
return false;
}
@Override
public boolean cancel() {
return true;
}
}
prepare方法用來準備資料,其實就是執行的實際任務。get方法用來返回任務的執行結果。
3)定義一個代理類FutureData用於給請求端FutureClient暫時返回一個假資料。等真實資料拿到之後,再裝載真實資料。
public class FutureData<T> implements Data<T>{
private RealData<T> realData ;
private boolean isReady = false;
private Thread runningThread;
public synchronized void setRealData(RealData realData) {
//如果已經裝載完畢了,就直接返回
if(isReady){
return;
}
//如果沒裝載,進行裝載真實物件
this.realData = realData;
isReady = true;
//進行通知
notify();
}
@Override
public synchronized T get() {
//如果沒裝載好 程式就一直處於阻塞狀態
while(!isReady){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//裝載好直接獲取資料即可
return realData.get();
}
public boolean isDone() {
return isReady;
}
@Override
public boolean cancel() {
if(isReady){
return false;
}
runningThread.interrupt();
return true;
}
public void setRunningThread(){
runningThread = Thread.currentThread();
}
}
如果get方法被呼叫,就會去判斷資料是否已經被載入好(即判斷isReady的值),如果沒有的話就呼叫wait方法進入等待。
setRealData用於去載入真實的資料,載入完畢之後就把isReady設定為true,然後呼叫notify方法通知正在等待的執行緒。此時,get方法收到通知就繼續執行,然後返回真實資料realData.get().
另外也簡單的實現了一個取消任務的方法cancel,去中斷正在執行子任務的執行緒。
4)FutureClient客戶端用於發起請求,非同步執行任務。
public class FutureClient {
public Data call(){
//建立一個代理物件FutureData,先返回給客戶端(無論是否有值)
final FutureData futureData = new FutureData();
//啟動一個新的執行緒,去非同步載入真實的物件
new Thread(new Runnable() {
@Override
public void run() {
//此處注意需要記錄一下非同步載入真實資料的執行緒,以便後續可以取消任務。
futureData.setRunningThread();
RealData realData = new RealData();
//等真實資料處理完畢之後,把結果賦值給代理物件
futureData.setRealData(realData);
}
}).start();
return futureData;
}
}
5)測試
public class Main {
static class Chuju{
}
static class Shicai{
}
public static void main(String[] args) throws InterruptedException {
FutureClient fc = new FutureClient();
Data data = fc.call();
Thread.sleep(2000);
Shicai shicai = new Shicai();
System.out.println("第二步:食材到位");
if(!data.isDone()){
System.out.println("第三步:廚具還沒到,請等待或者取消");
//②
// data.cancel();
// System.out.println("已取消");
// return;
}
//真正需要資料的時候,再去獲取
Chuju chuju = (Chuju)data.get();
System.out.println("第三步:廚具到位,可以烹飪了");
cook(chuju,shicai);
}
public static void cook (Chuju chuju, Shicai shicai){
System.out.println("最後:烹飪中...");
}
}
執行結果和用JDK提供的Future模式是一模一樣的。我們也可以把②出的程式碼開啟,測試任務取消的結果。
第一步:下單
第一步:等待送貨
第二步:食材到位
第三步:廚具還沒到,請等待或者取消
已取消
被中斷:java.lang.InterruptedException: sleep interrupted
執行取消之後,執行RealData的子執行緒就會被中斷,然後結束任務