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

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

Active Object模式的評價與實現考量

Active Object模式通過將方法的呼叫與執行分離,實現了非同步程式設計。有利於提高併發性,從而提高系統的吞吐率。

Active Object模式還有個好處是它可以將任務(MethodRequest)的提交(呼叫非同步方法)和任務的執行策略(Execution Policy)分離。任務的執行策略被封裝在Scheduler的實現類之內,因此它對外是不“可見”的,一旦需要變動也不會影響其它程式碼,降低了系統的耦合性。任務的執行策略可以反映以下一些問題:

  • 採用什麼順序去執行任務,如FIFO、LIFO、或者基於任務中包含的資訊所定的優先順序?
  • 多少個任務可以併發執行?
  • 多少個任務可以被排隊等待執行?
  • 如果有任務由於系統過載被拒絕,此時哪個任務該被選中作為犧牲品,應用程式該如何被通知到?
  • 任務執行前、執行後需要執行哪些操作?

這意味著,任務的執行順序可以和任務的提交順序不同,可以採用單執行緒也可以採用多執行緒去執行任務等等。

當然,好處的背後總是隱藏著代價,Active Object模式實現非同步程式設計也有其代價。該模式的參與者有6個之多,其實現過程也包含了不少中間的處理:MethodRequest物件的生成、MethodRequest物件的移動(進出緩衝區)、MethodRequest物件的執行排程和執行緒上下文切換等。這些處理都有其空間和時間的代價。因此,Active Object模式適合於分解一個比較耗時的任務(如涉及I/O操作的任務):將任務的發起和執行進行分離,以減少不必要的等待時間。

雖然模式的參與者較多,但正如本文案例的實現程式碼所展示的,其中大部分的參與者我們可以利用JDK自身提供的類來實現,以節省編碼時間。如表1所示。

表 1. 使用JDK現有類實現Active Object的一些參與者

參與者名稱 可以借用的JDK 備註
Scheduler Java Executor Framework中的java.util.concurrent.ExecutorService介面的相關實現類,如java.util.concurrent.ThreadPoolExecutor。 ExecutorService介面所定義的submit(Callable<T> task)方法相當於圖2中的enqueue方法。
ActivationQueue java.util.concurrent.LinkedBlockingQueue 若Scheduler採用java.util.concurrent.ThreadPoolExecutor,則java.util.concurrent.LinkedBlockingQueue例項作為ThreadPoolExecutor構造器的引數。
MethodRequest java.util.concurrent.Callable介面的匿名實現類。 Callable介面比起Runnable介面的優勢在於它定義的call方法有返回值,便於將該返回值傳遞給Future例項。
Future java.util.concurrent.Future ExecutorService介面所定義的submit(Callable<T> task)方法的返回值型別就是java.util.concurrent.Future。

錯誤隔離

錯誤隔離指一個任務的處理失敗不影響其它任務的處理。每個MethodRequest例項可以看作一個任務。那麼,Scheduler的實現類在執行MethodRequest時需要注意錯誤隔離。選用JDK中現成的類(如ThreadPoolExecutor)來實現Scheduler的一個好處就是這些類可能已經實現了錯誤隔離。而如果自己編寫程式碼實現Scheduler,用單個Active Object工作執行緒逐一執行所有任務,則需要特別注意執行緒的run方法的異常處理,確保不會因為個別任務執行時遇到一些執行時異常而導致整個執行緒終止。如清單6的示例程式碼所示。

清單 6. 自己動手實現Scheduler的錯誤隔離示例程式碼

public class CustomScheduler implements Runnable {
	private LinkedBlockingQueue<Runnable> activationQueue = 
		new LinkedBlockingQueue<Runnable>();

	@Override
	public void run() {
		dispatch();
	}

	public <T> Future<T> enqueue(Callable<T> methodRequest) {
		final FutureTask<T> task = new FutureTask<T>(methodRequest) {

			@Override
			public void run() {
				try {
				   super.run();
				//捕獲所以可能丟擲的物件,避免該任務執行失敗而導致其所在的執行緒終止。	
				} catch (Throwable t) {
				   this.setException(t);
				}
			}

		};

		try {
			activationQueue.put(task);
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		}
		return task;
	}

	public void dispatch() {
		while (true) {
			Runnable methodRequest;
			try {
				methodRequest = activationQueue.take();

				//防止個別任務執行失敗導致執行緒終止的程式碼在run方法中
				methodRequest.run();
			} catch (InterruptedException e) {
				// 處理該異常
			}

		}
	}
}

緩衝區監控

如果ActivationQueue是有界緩衝區,則對緩衝區的當前大小進行監控無論是對於運維還是測試來說都有其意義。從測試的角度來看,監控緩衝區有助於確定緩衝區容量的建議值(合理值)。清單3所示的程式碼,即是通過定時任務週期性地呼叫ThreadPoolExecutor的getQueue方法對緩衝區的大小進行監控。當然,在監控緩衝區的時候,往往只需要大致的值,因此在監控程式碼中要避免不必要的鎖。

緩衝區飽和處理策略

當任務的提交速率大於任務的執行數率時,緩衝區可能逐漸積壓到滿。這時新提交的任務會被拒絕。無論是自己編寫程式碼還是利用JDK現有類來實現Scheduler,對於緩衝區滿時新任務提交失敗,我們需要一個處理策略用於決定此時哪個任務會成為“犧牲品”。若使用ThreadPoolExecutor來實現Scheduler有個好處是它已經提供了幾個緩衝區飽和處理策略的實現程式碼,應用程式碼可以直接呼叫。如清單3的程式碼所示,本文案例中我們選擇了拋棄最老的任務作為處理策略。java.util.concurrent.RejectedExecutionHandler介面是ThreadPoolExecutor對緩衝區飽和處理策略的抽象,JDK中提供的具體實現如表2所示。

表 2. JDK提供的緩衝區飽和處理策略實現類

實現類 所實現的處理策略
ThreadPoolExecutor.AbortPolicy 直接丟擲異常。
ThreadPoolExecutor.DiscardPolicy 放棄當前被拒絕的任務(而不丟擲任何異常)。
ThreadPoolExecutor.DiscardOldestPolicy 將緩衝區中最老的任務放棄,然後重新嘗試接納被拒絕的任務。
ThreadPoolExecutor.CallerRunsPolicy 在任務的提交方執行緒中執行被拒絕的任務。

當然,對於ThreadPoolExecutor而言,其工作佇列滿不一定就意味著新提交的任務會被拒絕。當其最大執行緒池大小大於其核心執行緒池大小時,工作佇列滿的情況下,新提交的任務會用所有核心執行緒之外的新增執行緒來執行,直到工作執行緒數達到最大執行緒數時,新提交的任務會被拒絕。

Scheduler空閒工作執行緒清理

如果Scheduler採用多個工作執行緒(如採用ThreadPoolExecutor這樣的執行緒池)來執行任務。則可能需要清理空閒的執行緒以節約資源。清單3的程式碼就是直接使用了ThreadPoolExecutor的現有功能,在初始化其例項時通過指定其構造器的第3、4個引數( long keepAliveTime, TimeUnit unit),告訴ThreadPoolExecutor對於核心工作執行緒以外的執行緒若其已經空閒了指定時間,則將其清理掉。

可複用的Active Object模式實現

儘管利用JDK中的現成類可以極大地簡化Active Object模式的實現。但如果需要頻繁地在不同場景下使用Active Object模式,則需要一套更利於複用的程式碼,以節約編碼的時間和使程式碼更加易於理解。清單7展示一段基於Java動態代理的可複用的Active Object模式的Proxy參與者的實現程式碼。

清單 7. 可複用的Active Object模式Proxy參與者實現

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
public abstract class ActiveObjectProxy {

	private static class DispatchInvocationHandler implements InvocationHandler {
		private final Object delegate;
		private final ExecutorService scheduler;

		public DispatchInvocationHandler(Object delegate,
		    ExecutorService executorService) {
			this.delegate = delegate;
			this.scheduler = executorService;
		}

		private String makeDelegateMethodName(final Method method,
		    final Object[] arg) {
			String name = method.getName();
			name = "do" + Character.toUpperCase(name.charAt(0)) 
					+ name.substring(1);

			return name;
		}

		@Override
		public Object invoke(final Object proxy, final Method method,
		    final Object[] args) throws Throwable {

			Object returnValue = null;
			final Object delegate = this.delegate;
			final Method delegateMethod;

			//如果攔截到的被呼叫方法是非同步方法,則將其轉發到相應的doXXX方法
			if (Future.class.isAssignableFrom(method.getReturnType())) {
				delegateMethod = delegate.getClass().getMethod(
					makeDelegateMethodName(method, args),
					method.getParameterTypes());

				final ExecutorService scheduler = this.scheduler;

				Callable<Object> methodRequest = new Callable<Object>() {
					@Override
					public Object call() throws Exception {
						Object rv = null;

						try {
                          rv = delegateMethod.invoke(delegate, args);
						} catch (IllegalArgumentException e) {
							throw new Exception(e);
						} catch (IllegalAccessException e) {
							throw new Exception(e);
						} catch (InvocationTargetException e) {
							throw new Exception(e);
						}
						return rv;
					}
				};
				Future<Object> future = scheduler.submit(methodRequest);
				returnValue = future;

			} else {

				//若攔截到的方法呼叫不是非同步方法,則直接轉發
			delegateMethod = delegate.getClass()
			.getMethod(method.getName(),method.getParameterTypes());

				returnValue = delegateMethod.invoke(delegate, args);
			}

			return returnValue;
		}
	}

	/**
	 * 生成一個實現指定介面的Active Object proxy例項。
	 * 對interf所定義的非同步方法的呼叫會被裝發到servant的相應doXXX方法。
	 * @param interf 要實現的Active Object介面
	 * @param servant Active Object的Servant參與者例項
	 * @param scheduler Active Object的Scheduler參與者例項
	 * @return Active Object的Proxy參與者例項
	 */
	public static <T> T newInstance(Class<T> interf, Object servant,
	    ExecutorService scheduler) {

		@SuppressWarnings("unchecked")
		T f = (T) Proxy.newProxyInstance(interf.getClassLoader(),
		new Class[] { interf }, 
		new DispatchInvocationHandler(servant, scheduler));

		return f;
	}
}

清單7的程式碼實現了可複用的Active Object模式的Proxy參與者ActiveObjectProxy。ActiveObjectProxy通過使用Java動態代理,動態生成指定介面的代理物件。對該代理物件的非同步方法(即返回值型別為java.util.concurrent.Future的方法)的呼叫會被ActiveObjectProxy實現InvocationHandler(DispatchInvocationHandler)所攔截,並轉發給ActiveObjectProxy的newInstance方法中指定的Servant處理。

清單8所示的程式碼展示了通過使用ActiveObjectProxy快速Active Object模式。

清單 8. 基於可複用的API快速實現Active Object模式

public static void main(String[] args) throws 
	InterruptedException, ExecutionException {

	SampleActiveObject sao = ActiveObjectProxy.newInstance(
		    SampleActiveObject.class, new SampleActiveObjectImpl(),
		    Executors.newCachedThreadPool());
	Future<String> ft = sao.process("Something", 1);
	Thread.sleep(500);
	System.out.println(ft.get());

從清單8的程式碼可見,利用可複用的Active Object模式Proxy實現,應用開發人員只要指定Active Object模式對外保留的介面(對應ActiveObjectProxy.newInstance方法的第1個引數),並提供一個該介面的實現類(對應ActiveObjectProxy.newInstance方法的第2個引數),再指定一個java.util.concurrent.ExecutorService例項(對應ActiveObjectProxy.newInstance方法的第3個引數)即可以實現Active Object模式。

總結

本文介紹了Active Object模式的意圖及架構。並提供了一個實際的案例用於展示使用Java程式碼實現Active Object模式,在此基礎上對該模式進行了評價並分享了在實際運用該模式時需要注意的事項。

參考資源

  • 本文的原始碼線上閱讀:https://github.com/Viscent/JavaConcurrencyPattern/
  • 維基百科Active Object模式詞條:http://en.wikipedia.org/wiki/Active_object
  • Douglas C. Schmidt對Active Object模式的定義:http://www.laputan.org/pub/sag/act-obj.pdf。
  • Schmidt, Douglas et al. Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects. Volume 2. Wiley, 2000
  • Java theory and practice: Decorating with dynamic proxies:http://www.ibm.com/developerworks/java/library/j-jtp08305/index.html

相關文章