本文由黃文海首次發布在infoq中文站上:http://www.infoq.com/cn/articles/Java-multithreaded-programming-mode-active-object-part1 。轉載請註明作者: 黃文海 出處:http://viscent.iteye.com。
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;@Overridepublic 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.Proxypublic 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.Servantprivate final DiskbasedRequestPersistence delegate = new DiskbasedRequestPersistence();// ActiveObjectPattern.Schedulerprivate 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.ActivationQueuenew LinkedBlockingQueue(200), new ThreadFactory() {@Overridepublic 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() {@Overridepublic 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;}@Overridepublic void store(final MMSDeliverRequest request) {/* * 將對store方法的調用封裝成MethodRequest對象, 並存入緩衝區。 */// ActiveObjectPattern.MethodRequestCallable methodRequest = new Callable() {@Overridepublic 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);@Overridepublic 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模式時需要注意的一些事項。
感謝張龍對本文的審校。