Java多執行緒程式設計模式實戰指南:Active Object模式(上)

InfoQ - 黃文海發表於2014-11-29

Active Object模式簡介

Active Object模式是一種非同步程式設計模式。它通過對方法的呼叫與方法的執行進行解耦來提高併發性。若以任務的概念來說,Active Object模式的核心則是它允許任務的提交(相當於對非同步方法的呼叫)和任務的執行(相當於非同步方法的真正執行)分離。這有點類似於System.gc()這個方法:客戶端程式碼呼叫完gc()後,一個進行垃圾回收的任務被提交,但此時JVM並不一定進行了垃圾回收,而可能是在gc()方法呼叫返回後的某段時間才開始執行任務——回收垃圾。我們知道,System.gc()的呼叫方程式碼是執行在自己的執行緒上(通常是main執行緒派生的子執行緒),而JVM的垃圾回收這個動作則由專門的執行緒(垃圾回收執行緒)來執行的。換言之,System.gc()這個方法所代表的動作(其所定義的功能)的呼叫方和執行方是執行在不同的執行緒中的,從而提高了併發性。

再進一步介紹Active Object模式,我們可先簡單地將其核心理解為一個名為ActiveObject的類,該類對外暴露了一些非同步方法,如圖1所示。

圖 1. ActiveObject物件示例

doSomething方法的呼叫方和執行方執行在各自的執行緒上。在併發的環境下,doSomething方法會被多個執行緒呼叫。這時所需的執行緒安全控制封裝在doSomething方法背後,使得呼叫方程式碼無需關心這點,從而簡化了呼叫方程式碼:從呼叫方程式碼來看,呼叫一個Active Object物件的方法與呼叫普通Java物件的方法並無太大差別。如清單1所示。

清單 1. Active Object方法呼叫示例

ActiveObject ao=...;
Future future = ao.doSomething("data");
//執行其它操作
String result = future.get();
System.out.println(result);

Active Object模式的架構

當Active Object模式對外暴露的非同步方法被呼叫時,與該方法呼叫相關的上下文資訊,包括被呼叫的非同步方法名(或其代表的操作)、呼叫方程式碼所傳遞的引數等,會被封裝成一個物件。該物件被稱為方法請求(Method Request)。方法請求物件會被存入Active Object模式所維護的緩衝區(Activation Queue)中,並由專門的工作執行緒負責根據其包含的上下文資訊執行相應的操作。也就是說,方法請求物件是由執行呼叫方程式碼的執行緒通過呼叫Active Object模式對外暴露的非同步方法生成的,而方法請求所代表的操作則由專門的執行緒來執行,從而實現了方法的呼叫與執行的分離,產生了併發。

Active Object模式的主要參與者有以下幾種。其類圖如圖2所示。

圖 2. Active Object模式的類圖

(點選影像放大)

  • Proxy:負責對外暴露非同步方法介面。當呼叫方程式碼呼叫該參與者例項的非同步方法doSomething時,該方法會生成一個相應的MethodRequest例項並將其儲存到Scheduler所維護的緩衝區中。doSomething方法的返回值是一個表示其執行結果的外包裝物件:Future參與者的例項。非同步方法doSomething執行在呼叫方程式碼所在的執行緒中。
  • MethodRequest:負責將呼叫方程式碼對Proxy例項的非同步方法的呼叫封裝為一個物件。該物件保留了非同步方法的名稱及呼叫方程式碼傳遞的引數等上下文資訊。它使得將Proxy的非同步方法的呼叫和執行分離成為可能。其call方法會根據其所包含上下文資訊呼叫Servant例項的相應方法。
  • ActivationQueue:負責臨時儲存由Proxy的非同步方法被呼叫時所建立的MethodRequest例項的緩衝區。
  • Scheduler:負責將Proxy的非同步方法所建立的MethodRequest例項存入其維護的緩衝區中。並根據一定的排程策略,對其維護的緩衝區中的MethodRequest例項進行執行。其排程策略可以根據實際需要來定,如FIFO、LIFO和根據MethodRequest中包含的資訊所定的優先順序等。
  • Servant:負責對Proxy所暴露的非同步方法的具體實現。
  • Future:負責儲存和返回Active Object非同步方法的執行結果。

Active Object模式的序列圖如圖3所示。

圖 3. Active Object模式的序列圖

(點選影像放大)

第1步:呼叫方程式碼呼叫Proxy的非同步方法doSomething。

第2~7步:doSomething方法建立Future例項作為該方法的返回值。並將呼叫方程式碼對該方法的呼叫封裝為MethodRequest物件。然後以所建立的MethodRequest物件作為引數呼叫Scheduler的enqueue方法,以將MethodRequest物件存入緩衝區。Scheduler的enqueue方法會呼叫Scheduler所維護的ActivationQueue例項的enqueue方法,將MethodRequest物件存入緩衝區。

第8步:doSomething返回其所建立的Future例項。

第9步:Scheduler例項採用專門的工作執行緒執行dispatch方法。

第10~12步:dispatch方法呼叫ActivationQueue例項的dequeue方法,獲取一個MethodRequest物件。然後呼叫MethodRequest物件的call方法

第13~16步:MethodRequest物件的call方法呼叫與其關聯的Servant例項的相應方法doSomething。並將Servant.doSomething方法的返回值設定到Future例項上。

第17步:MethodRequest物件的call方法返回。

上述步驟中,第1~8步是執行在Active Object的呼叫者執行緒中的,這幾個步驟實現了將呼叫方程式碼對Active Object所提供的非同步方法的呼叫封裝成物件(Method Request),並將其存入緩衝區。這幾個步驟實現了任務的提交。第9~17步是執行在Active Object的工作執行緒中,這些步驟實現從緩衝區中讀取Method Request,並對其進行執行,實現了任務的執行。從而實現了Active Object對外暴露的非同步方法的呼叫與執行的分離。

如果呼叫方程式碼關心Active Object的非同步方法的返回值,則可以在其需要時,呼叫Future例項的get方法來獲得非同步方法的真正執行結果。

Active Object模式實戰案例

某電信軟體有一個彩信短號模組。其主要功能是實現手機使用者給其它手機使用者傳送彩信時,接收方號碼可以填寫為對方的短號。例如,使用者13612345678給其同事13787654321傳送彩信時,可以將接收方號碼填寫為對方的短號,如776,而非其真實的號碼。

該模組處理其接收到的下發彩信請求的一個關鍵操作是查詢資料庫以獲得接收方短號對應的真實號碼(長號)。該操作可能因為資料庫故障而失敗,從而使整個請求無法繼續被處理。而資料庫故障是可恢復的故障,因此在短號轉換為長號的過程中如果出現資料庫異常,可以先將整個下發彩信請求訊息快取到磁碟中,等到資料庫恢復後,再從磁碟中讀取請求訊息,進行重試。為方便起見,我們可以通過Java的物件序列化API,將表示下發彩信的物件序列化到磁碟檔案中從而實現請求快取。下面我們討論這個請求快取操作還需要考慮的其它因素,以及Active Object模式如何幫助我們滿足這些考慮。

首先,請求訊息快取到磁碟中涉及檔案I/O這種慢的操作,我們不希望它在請求處理的主執行緒(即Web伺服器的工作執行緒)中執行。因為這樣會使該模組的響應延時增大,降低系統的響應性。並使得Web伺服器的工作執行緒因等待檔案I/O而降低了系統的吞吐量。這時,非同步處理就派上用場了。Active Object模式可以幫助我們實現請求快取這個任務的提交和執行分離:任務的提交是在Web伺服器的工作執行緒中完成,而任務的執行(包括序列化物件到磁碟檔案中等操作)則是在Active Object工作執行緒中執行。這樣,請求處理的主執行緒在偵測到短號轉長號失敗時即可以觸發對當前彩信下發請求進行快取,接著繼續其請求處理,如給客戶端響應。而此時,當前請求訊息可能正在被Active Object執行緒快取到檔案中。如圖4所示。

圖 4 .非同步實現快取

其次,每個短號轉長號失敗的彩信下發請求訊息會被快取為一個磁碟檔案。但我們不希望這些快取檔案被存在同一個子目錄下。而是希望多個快取檔案會被儲存到多個子目錄中。每個子目錄最多可以儲存指定個數(如2000個)的快取檔案。若當前子目錄已存滿,則新建一個子目錄存放新的快取檔案,直到該子目錄也存滿,依此類推。當這些子目錄的個數到達指定數量(如100個)時,最老的子目錄(連同其下的快取檔案,如果有的話)會被刪除。從而保證子目錄的個數也是固定的。顯然,在併發環境下,實現這種控制需要一些併發訪問控制(如通過鎖來控制),但是我們不希望這種控制暴露給處理請求的其它程式碼。而Active Object模式中的Proxy參與者可以幫助我們封裝併發訪問控制。

下面,我們看該案例的相關程式碼通過應用Active Object模式在實現快取功能時滿足上述兩個目標。首先看請求處理的入口類。該類就是本案例的Active Object模式的客呼叫方程式碼。如清單2所示。

清單 2. 彩信下發請求處理的入口類

public class MMSDeliveryServlet extends HttpServlet {

	private static final long serialVersionUID = 5886933373599895099L;

	@Override
	public void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		//將請求中的資料解析為內部物件
		MMSDeliverRequest mmsDeliverReq = this.parseRequest(req.getInputStream());
		Recipient shortNumberRecipient = mmsDeliverReq.getRecipient();
		Recipient originalNumberRecipient = null;

		try {
			// 將接收方短號轉換為長號
			originalNumberRecipient = convertShortNumber(shortNumberRecipient);
		} catch (SQLException e) {

			// 接收方短號轉換為長號時發生資料庫異常,觸發請求訊息的快取
			AsyncRequestPersistence.getInstance().store(mmsDeliverReq);

			// 繼續對當前請求的其它處理,如給客戶端響應
			resp.setStatus(202);
		}

	}

	private MMSDeliverRequest parseRequest(InputStream reqInputStream) {
		MMSDeliverRequest mmsDeliverReq = new MMSDeliverRequest();
		//省略其它程式碼
		return mmsDeliverReq;
	}

	private Recipient convertShortNumber(Recipient shortNumberRecipient)
			throws SQLException {
		Recipient recipent = null;
		//省略其它程式碼
		return recipent;
	}

}

清單2中的doPost方法在偵測到短號轉換過程中發生的資料庫異常後,通過呼叫AsyncRequestPersistence類的store方法觸發對彩信下發請求訊息的快取。這裡,AsyncRequestPersistence類相當於Active Object模式中的Proxy參與者。儘管本案例涉及的是一個併發環境,但從清單2中的程式碼可見,AsyncRequestPersistence類的呼叫方程式碼無需處理多執行緒同步問題。這是因為多執行緒同步問題被封裝在AsyncRequestPersistence類之後。

AsyncRequestPersistence類的程式碼如清單3所示。

清單 3. 彩信下發請求快取入口類(Active Object模式的Proxy)

// ActiveObjectPattern.Proxy
public class AsyncRequestPersistence implements RequestPersistence {
	private static final long ONE_MINUTE_IN_SECONDS = 60;
	private final Logger logger;
	private final AtomicLong taskTimeConsumedPerInterval = new AtomicLong(0);
	private final AtomicInteger requestSubmittedPerIterval = new AtomicInteger(0);

	// ActiveObjectPattern.Servant
	private final DiskbasedRequestPersistence 
						delegate = new DiskbasedRequestPersistence();
	// ActiveObjectPattern.Scheduler
	private final ThreadPoolExecutor scheduler;

	private static class InstanceHolder {
		final static RequestPersistence INSTANCE = new AsyncRequestPersistence();
	}

	private AsyncRequestPersistence() {
		logger = Logger.getLogger(AsyncRequestPersistence.class);
		scheduler = new ThreadPoolExecutor(1, 3, 
				60 * ONE_MINUTE_IN_SECONDS,
				TimeUnit.SECONDS,
				// ActiveObjectPattern.ActivationQueue
				new LinkedBlockingQueue(200), 
				new ThreadFactory() {
					@Override
					public Thread newThread(Runnable r) {
						Thread t;
						t = new Thread(r, "AsyncRequestPersistence");
						return t;
					}

				});

		scheduler.setRejectedExecutionHandler(
				new ThreadPoolExecutor.DiscardOldestPolicy());

		// 啟動佇列監控定時任務
		Timer monitorTimer = new Timer(true);
		monitorTimer.scheduleAtFixedRate(
		    new TimerTask() {

			@Override
			public void run() {
				if (logger.isInfoEnabled()) {

					logger.info("task count:" 
							+ requestSubmittedPerIterval
							+ ",Queue size:" 
							+ scheduler.getQueue().size()
							+ ",taskTimeConsumedPerInterval:"
							+ taskTimeConsumedPerInterval.get() 
							+ " ms");
				}

				taskTimeConsumedPerInterval.set(0);
				requestSubmittedPerIterval.set(0);

			}
		}, 0, ONE_MINUTE_IN_SECONDS * 1000);
	}

	public static RequestPersistence getInstance() {
		return InstanceHolder.INSTANCE;
	}

	@Override
	public void store(final MMSDeliverRequest request) {
		/*
		 * 將對store方法的呼叫封裝成MethodRequest物件, 並存入緩衝區。
		 */
		// ActiveObjectPattern.MethodRequest
		Callable methodRequest = new Callable() {
			@Override
			public Boolean call() throws Exception {
				long start = System.currentTimeMillis();
				try {
					delegate.store(request);
				} finally {
					taskTimeConsumedPerInterval.addAndGet(
							System.currentTimeMillis() - start);
				}

				return Boolean.TRUE;
			}

		};
		scheduler.submit(methodRequest);

		requestSubmittedPerIterval.incrementAndGet();
	}

}

AsyncRequestPersistence類所實現的介面RequestPersistence定義了Active Object對外暴露的非同步方法:store方法。由於本案例不關心請求快取的結果,故該方法沒有返回值。其程式碼如清單4所示。

清單 4. RequestPersistence介面原始碼

public interface RequestPersistence {

	 void store(MMSDeliverRequest request);
}

AsyncRequestPersistence類的例項變數scheduler相當於Active Object模式中的Scheduler參與者例項。這裡我們直接使用了JDK1.5引入的Executor Framework中的ThreadPoolExecutor。在ThreadPoolExecutor類的例項化時,其構造器的第5個引數(BlockingQueue<Runnable> workQueue)我們指定了一個有界阻塞佇列:new LinkedBlockingQueue<Runnable>(200)。該佇列相當於Active Object模式中的ActivationQueue參與者例項。

AsyncRequestPersistence類的例項變數delegate相當於Active Object模式中的Servant參與者例項。

AsyncRequestPersistence類的store方法利用匿名類生成一個java.util.concurrent.Callable例項methodRequest。該例項相當於Active Object模式中的MethodRequest參與者例項。利用閉包(Closure),該例項封裝了對store方法呼叫的上下文資訊(包括呼叫引數、所呼叫的方法對應的操作資訊)。AsyncRequestPersistence類的store方法通過呼叫scheduler的submit方法,將methodRequest送入ThreadPoolExecutor所維護的緩衝區(阻塞佇列)中。確切地說,ThreadPoolExecutor是Scheduler參與者的一個“近似”實現。ThreadPoolExecutor的submit方法相對於Scheduler的enqueue方法,該方法用於接納MethodRequest物件,以將其存入緩衝區。當ThreadPoolExecutor當前使用的執行緒數量小於其核心執行緒數量時,submit方法所接收的任務會直接被新建的執行緒執行。當ThreadPoolExecutor當前使用的執行緒數量大於其核心執行緒數時,submit方法所接收的任務才會被存入其維護的阻塞佇列中。不過,ThreadPoolExecutor的這種任務處理機制,並不妨礙我們將它用作Scheduler的實現。

methodRequest的call方法會呼叫delegate的store方法來真正實現請求快取功能。delegate例項對應的類DiskbasedRequestPersistence是請求訊息快取功能的真正實現者。其程式碼如清單5所示。

清單 5. DiskbasedRequestPersistence類的原始碼

public class DiskbasedRequestPersistence implements RequestPersistence {
	// 負責快取檔案的儲存管理
	private final SectionBasedDiskStorage storage = new SectionBasedDiskStorage();
	private final Logger logger = Logger
	                                 .getLogger(DiskbasedRequestPersistence.class);

	@Override
	public void store(MMSDeliverRequest request) {
		// 申請快取檔案的檔名
		String[] fileNameParts = storage.apply4Filename(request);
		File file = new File(fileNameParts[0]);
		try {
			ObjectOutputStream objOut = new ObjectOutputStream(
			new FileOutputStream(file));
			try {
				objOut.writeObject(request);
			} finally {
				objOut.close();
			}
		} catch (FileNotFoundException e) {
			storage.decrementSectionFileCount(fileNameParts[1]);
			logger.error("Failed to store request", e);
		} catch (IOException e) {
			storage.decrementSectionFileCount(fileNameParts[1]);
			logger.error("Failed to store request", e);
		}

	}

	class SectionBasedDiskStorage {
		private Deque sectionNames = new LinkedList();
		/*
		 * Key->value: 儲存子目錄名->子目錄下快取檔案計數器
		 */
		private Map sectionFileCountMap 
						= new HashMap();
		private int maxFilesPerSection = 2000;
		private int maxSectionCount = 100;
		private String storageBaseDir = System.getProperty("user.dir") + "/vpn";

		private final Object sectionLock = new Object();

		public String[] apply4Filename(MMSDeliverRequest request) {
			String sectionName;
			int iFileCount;
			boolean need2RemoveSection = false;
			String[] fileName = new String[2];
			synchronized (sectionLock) {
				//獲取當前的儲存子目錄名
				sectionName = this.getSectionName();
				AtomicInteger fileCount;
				fileCount = sectionFileCountMap.get(sectionName);
				iFileCount = fileCount.get();
				//當前儲存子目錄已滿
				if (iFileCount >= maxFilesPerSection) {
					if (sectionNames.size() >= maxSectionCount) {
						need2RemoveSection = true;
					}
					//建立新的儲存子目錄
					sectionName = this.makeNewSectionDir();
					fileCount = sectionFileCountMap.get(sectionName);

				}
				iFileCount = fileCount.addAndGet(1);

			}

			fileName[0] = storageBaseDir + "/" + sectionName + "/"
			    + new DecimalFormat("0000").format(iFileCount) + "-"
			    + request.getTimeStamp().getTime() / 1000 + "-" 
                               + request.getExpiry()
			    + ".rq";
			fileName[1] = sectionName;

			if (need2RemoveSection) {
				//刪除最老的儲存子目錄
				String oldestSectionName = sectionNames.removeFirst();
				this.removeSection(oldestSectionName);
			}

			return fileName;
		}

		public void decrementSectionFileCount(String sectionName) {
			AtomicInteger fileCount = sectionFileCountMap.get(sectionName);
			if (null != fileCount) {
				fileCount.decrementAndGet();
			}
		}

		private boolean removeSection(String sectionName) {
			boolean result = true;
			File dir = new File(storageBaseDir + "/" + sectionName);
			for (File file : dir.listFiles()) {
				result = result && file.delete();
			}
			result = result && dir.delete();
			return result;
		}

		private String getSectionName() {
			String sectionName;

			if (sectionNames.isEmpty()) {
				sectionName = this.makeNewSectionDir();

			} else {
				sectionName = sectionNames.getLast();
			}

			return sectionName;
		}

		private String makeNewSectionDir() {
			String sectionName;
			SimpleDateFormat sdf = new SimpleDateFormat("MMddHHmmss");
			sectionName = sdf.format(new Date());
			File dir = new File(storageBaseDir + "/" + sectionName);
			if (dir.mkdir()) {
				sectionNames.addLast(sectionName);
				sectionFileCountMap.put(sectionName, new AtomicInteger(0));
			} else {
				throw new RuntimeException(
                                 "Cannot create section dir " + sectionName);
			}

			return sectionName;
		}
	}
}

methodRequest的call方法的呼叫者程式碼是執行在ThreadPoolExecutor所維護的工作者執行緒中,這就保證了store方法的呼叫方和真正的執行方是分別執行在不同的執行緒中:伺服器工作執行緒負責觸發請求訊息快取,ThreadPoolExecutor所維護的工作執行緒負責將請求訊息序列化到磁碟檔案中。

DiskbasedRequestPersistence類的store方法中呼叫的SectionBasedDiskStorage類的apply4Filename方法包含了一些多執行緒同步控制程式碼(見清單5)。這部分控制由於是封裝在DiskbasedRequestPersistence的內部類中,對於該類之外的程式碼是不可見的。因此,AsyncRequestPersistence的呼叫方程式碼無法知道該細節,這體現了Active Object模式對併發訪問控制的封裝。

小結

本篇介紹了Active Object模式的意圖及架構,並以一個實際的案例展示了該模式的程式碼實現。下篇將對Active Object模式進行評價,並結合本文案例介紹實際運用Active Object模式時需要注意的一些事項。

相關文章