怎么用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的子線程就會被中斷,然后結束任務。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM