說一說併發設計模式—Future(非同步)

擁抱心中的夢想發表於2019-03-04

網上講解該設計模式的文章非常地多,很多講的內容、技術都比我全面和深入!但既然學習,我還是想進行一下總結!希望閱讀本文章的你可以獲得一點啟發!

說到併發程式設計,我們都會覺得,哇,併發程式設計是真的難!可事實真的是那樣子嗎?我覺得不是,一門技術出來,只要你有那個決心去攻克它,那麼你就贏了。也就是我們常說的:“穩住,猥瑣發育,別浪,我們能贏!”,學習的過程中肯定會遇到一些難點,多找人溝通交流,相互進步是非常好的途徑,小編希望你們在閱讀我總結的文章之後能和小編一起交流,不甚榮幸。

一、什麼是非同步?

講解設計模式之前我先講下非同步,非同步和同步是對立的,這兩個概念其實是不難理解的,關鍵是同步和非同步究竟是怎樣在程式中體現,才是我們最好奇,最想知道的。

同步:就是當任務A依賴於任務B的執行時,必須等待任務B執行完畢之後任務A才繼續執行,此過程任務A被阻塞。任務要麼都成功,要麼都失敗!想一想我們打電話的情景即可!
非同步:任務A呼叫任務B,任務A不需要等到任務B執行完畢,任務B只是返回一個虛擬的結果給任務A,使得任務A能夠繼續做其他事情,等到任務B執行完成之後再通知任務A(回撥)或者是任務A主動去請求任務B要結果。想一想發簡訊的情景即可!

二、Future非同步設計模式

首先我在網上找到了一張圖,分別對比了非同步同步的時序區別:

說一說併發設計模式—Future(非同步)

那麼我們怎麼實現呢?先貼一張UML圖再說:

說一說併發設計模式—Future(非同步)

講解例項之前,我覺得有必要說一些先決條件:

  • 1、notify/wait的使用,每個java物件都擁有這兩個方法,呼叫這兩個方法必須先獲得監視器,常用的做法是在synchronized同步塊中進行呼叫
  • 2、生產者/消費者模型,其實這個可有可無,主要是不要寫出在if(塊中)呼叫notify/wait方法,而要改為while

我所有的邏輯註釋都穿插在下面的程式碼中,如果有想要一起交流的問題,歡迎評論,歡迎評論,歡迎評論!

Main.java

package com.wokao66.future;

/**
 * 持有一個客戶端Client物件,傳送請求
 * @author: huangjiawei
 * @since: 2018年4月2日
 * @version: $Revision$ $Date$ $LastChangedBy$
 */
public class Main {
	public static void main(String[] args) throws Exception {
		/**
		 * 持有一個客戶端Client物件	
		 */
		Client client = new Client();
		/**
		 * 返回一個虛擬的資料(這是非同步返回的,虛擬的,不是真實的,但必須持有真實資料物件realData,方便後面獲取請求結果)
		 */
		Data virtualData = client.request("我要下單!!!!");
		/**
		 * 睡眠5秒
		 */
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {}
		/**
		 * 我現在想獲得真實資料了
		 */
		String realData = virtualData.getResultData();
		System.err.println("真實資料為:" + realData);
	}
}
複製程式碼

Data.java

package com.wokao66.future;

/**
 * 對返回資料的簡單抽象(虛擬資料和真實資料都必須實現該介面)
 * @author: huangjiawei
 * @since: 2018年4月2日
 * @version: $Revision$ $Date$ $LastChangedBy$
 */
public interface Data {

	/**
	 * 獲取資料的操作,至於是虛擬的,還是真實的,我不用管,讓實現類去決定
	 * @throws Exception 
	 */
	public abstract String getResultData() throws Exception;
}
複製程式碼

VirtualData.java

package com.wokao66.future;

/**
 * 虛擬的資料(非同步返回給客戶端)
 * @author: huangjiawei
 * @since: 2018年4月2日
 * @version: $Revision$ $Date$ $LastChangedBy$
 */
public class VirtualData implements Data {

	/**
	 * 想象一下,如果你這裡返回的VirtualData不包含對RealData的引用,那麼當客戶端需要獲取真實資料時,你的資料從何而來???
	 */
	private RealData realData = null;
	/**
	 * 預設是還沒有準備好資料嘛!要有個狀態來跟蹤
	 */
	private boolean isReady = false;

	/**
	 * 注入RealData,這個RealData
	 * @param realData
	 */
	public synchronized void setRealData(RealData realData) {
		System.err.println("獲得鎖");
		/**
		 * 如果還沒有準備好,我就需要
		 */
		if (isReady) {
			return;
		}
		this.realData = realData;
		isReady = true;//我已經準備好了
		notify();//通知所有阻塞的執行緒
	}

	/**
	 * 重寫獲取資料的方式
	 * @throws Exception 
	 */
	@Override
	public synchronized String getResultData() throws Exception {
		/**
		 * 如果客戶端呼叫的時候我還沒有注入真實資料,那麼就一直阻塞
		 */
		while (!isReady) {
			//呼叫wait必須先獲得物件的鎖,所以
			wait();
		}
		return realData.getResultData();

	}
}
複製程式碼

RealData.java

package com.wokao66.future;

/**
 * 這個類表示你具體的業務操作,比如重資料庫查詢資料
 * @author: huangjiawei
 * @since: 2018年4月2日
 * @version: $Revision$ $Date$ $LastChangedBy$
 */
public class RealData implements Data {
	/**
	 * 請求名
	 */
	private String readData;
	public RealData(String readData) {
		/**
		 * 我這裡先休眠10秒,表示一個耗時的操作
		 */
		try {
			Thread.sleep(10000);
		} catch (InterruptedException e) {}

		this.readData = "呼叫名為 : " + readData + " , " + "真實的資料為 : realData";
	}
	@Override
	public String getResultData() {
		return readData;
	}
}
複製程式碼

Client.java

package com.wokao66.future;

/**
 * 表示我們的客戶端程式嘛!負責發起呼叫請求
 * @author: huangjiawei
 * @since: 2018年4月2日
 * @version: $Revision$ $Date$ $LastChangedBy$
 */
public class Client {

	/**
	 * 表示客戶端的請求
	 * @param name 具體的請求名稱
	 * @return
	 */
	public Data request(String name) {
		/**
		 * 宣告一個虛擬的資料
		 */
		VirtualData virtualData = new VirtualData();
		/**
		 * 當你呼叫請求時,我後臺默默開啟一個執行緒去處理真實操作
		 */
		new Thread(new Runnable() {

			@Override
			public void run() {
				System.err.println("我偷偷摸摸地請求後臺獲取資料的操作,該操作可能會執行很長的時間");
				System.err.println("我不管了,先返回結果給呼叫方");
				RealData realData = new RealData(name);
				//下面兩句的執行順序是不一樣的
				//啟動執行緒
				virtualData.setRealData(realData);
			}
		}).start();
		//我先返回一個虛擬的資料給你,真的資料等我獲取完成之後你再過來取
		return virtualData;
	}
}
複製程式碼

執行結果

下單成功...............
我偷偷摸摸地請求後臺獲取資料的操作,該操作可能會執行很長的時間
我不管了,先返回結果給呼叫方
獲得鎖
真實資料為:呼叫名為 : 我要下單!!!! , 真實的資料為 : realData
複製程式碼

不知道大家有沒有思考過,上面Main.javaString realData = virtualData.getResultData();是一個本地物件,只能在本機執行,那如果是基於tcp的網路傳輸呢?可以採用個推,websocket等方式進行,dubbo目測也是可以的!對了,突然想起來,Java EE 7開始已經支援非同步Servlet了,詳細的可以自行檢視相關文件!

三、Java 中的非同步併發程式設計

非同步執行結果無非就有兩種形式:

  • 1、執行之後有回撥
  • 2、執行之後沒有回撥

有回撥指的是客戶端建立相應的監聽器Listener,服務端執行非同步Future之後主動回撥客戶端的回撥函式

無回撥即我們前面例子講的,我們需要自己主動去請求服務端判斷是否已經成功了

package com.wokao66.javafuture;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Test {

	public static void main(String[] args) {
		/**
		 * 新建一個執行緒池
		 */
		ExecutorService executor = Executors.newFixedThreadPool(1);
		/**
		 * 建立一個任務
		 */
		Task task = new Task();
		/**
		 * 將任務提交給執行緒池
		 */
		Future<Integer> result = executor.submit(task);
		//這裡會保證所有子執行緒執行完畢再關閉
		executor.shutdown();

		try {
			/**
			 * 模擬執行其它操作
			 */
			Thread.sleep(1000);
		} catch (InterruptedException e1) {
			e1.printStackTrace();
		}
		System.out.println("我在執行其他操作........");

		try {
			System.out.println("任務的執行結果是:" + result.get());
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (ExecutionException e) {
			e.printStackTrace();
		}

	}
}

class Task implements Callable<Integer> {

	@Override
	public Integer call() throws Exception {
		System.out.println("子執行緒在進行計算");
		/**
		 * 模擬執行耗時操作
		 */
		Thread.sleep(10000);

		return 100;
	}
}
複製程式碼

執行結果

子執行緒在進行計算
我在執行其他操作........
任務的執行結果是:100
複製程式碼

同時java也可以使用FutureTask來建立任務,FutureTask同時實現RunnableCallable介面,非常方便!

謝謝閱讀!

相關文章