逾時控制是我們在系統中容易忽視卻又不能忽視的功能。一方面,逾時並不是我們系統的主要業務功能,大多數逾時控制已經被虛擬機器或者伺服器實現了;另一方面,逾時控制是許多業務必備的隱性功能,試想,一個web應用如果在10秒預設逾時過後才響應,客戶能夠忍受嗎。
這篇文章主要總結了一些常見的逾時處理機制,以及適用的情境。
首先這裡提出一個實際遇到的問題,大家可以思考如何解決。在調用corba串連伺服器並擷取資料時需要做逾時處理,以便給使用者一個良好的使用者體驗(如service.handshake(),握手的過程就是發起corba請求的過程),可惜的是,corba產生的程式碼並沒有提供一個逾時的參數入口(查過一下,似乎在corba中串連要設定逾時必須配置他的policy,但是我對此一竅不通),在這種情況下,如何用我們已知的方法來完成這個任務呢。
其實這個問題基本思路跟線程有關係,因為逾時意味著阻塞,如果只有主線程,則阻塞就意味著失去了控制,基本上就談不上逾時控制了(個人認為線上程運行中間自己判斷並跳出迴圈的方式只是線程的操作方法,並不算逾時控制)。如果有主從兩個線程,則在從線程阻塞過程中知道從線程逾時並將其終止,則達到我們的目的。最終的思想就是將其他逾時問題轉化為線程逾時問題。
我們來看第一種方式,即網上常見的join方式。因為沒有直接的線程方法提供逾時參數,則可以轉換下思維,利用已經提供逾時的方法join。join是Thread類的方法,意為當前線程等待調用線程結束,最重要的是,join可以傳入逾時控制的參數,參照如下兩個原型:
join(long millis) join(long millis, int nanos)
那麼是否可以在子線程中完成待逾時任務,並join子線程,當join逾時便回到主線程,然後在主線程中終止子線程。HttpClient中TimeoutController就是採用這個方式。這裡task是子線程,可以完成類似corba串連之類的任務,在task join之後,當前運行線程便會等待task運行結束,並且只等待timeout時間,如果timeout過了子線程仍然沒有結束,則主線程可以活過來並中斷子線程。
public static void execute(Thread task, long timeout) throws TimeoutException { task.start(); try { task.join(timeout); } catch (InterruptedException e) { /* if somebody interrupts us he knowswhat he is doing */ } if (task.isAlive()) { task.interrupt(); throw new TimeoutException(); } } 第一種方式沒有那麼直觀,相對來說,join有些繞彎,有沒有更直接點的方式。那就是用concurrent包的Future,future在得到計算結果時有個逾時參數,利用此方式,可以有效實現逾時
ExecutorService executor = Executors.newSingleThreadExecutor(); FutureTask<LicenseService> future = new FutureTask<LicenseService>( new Callable<LicenseService>() { public LicenseService call() { LicenseServerAdapter adapter = new LicenseServerAdapter( info); LicenseService service = adapter.getLicenseService(); service.handShake(); return service; } }); executor.execute(future); return future.get(timeout * 1000, TimeUnit.MILLISECONDS);
說到逾時,還有另外一個逾時的情境,即web伺服器的session逾時。可以想象一下,如果自己實現了web伺服器,需要實現session的功能,這時該如何?session的逾時呢。這個問題可以有很多方式解決,這裡提供兩個,第一是在每個session對象中存入一個時間戳記,每次訪問時先檢查是否已經到期,如到期,則回收該session,如沒有到期,則更新該時間戳記,但是有個缺點,如果在到期後都不再次訪問,則session永遠不會被回收,當然,這裡還是可以改進的,比如每次訪問時檢查所有session是否到期,又會帶來效率的問題,有興趣的朋友可以思考一下。另外一個方式是tomcat的實現,即一個後台線程定時對session做掃描,在掃描過程中實施session的時間戳記更新或回收問題,參看代碼
public void backgroundProcess() { count = (count + 1) % processExpiresFrequency; if (count == 0) processExpires(); } /** * Invalidate all sessions that have expired. */ public void processExpires() { long timeNow = System.currentTimeMillis(); Session sessions[] = findSessions(); int expireHere = 0 ; if(log.isDebugEnabled()) log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length); for (int i = 0; i < sessions.length; i++) { if (sessions[i]!=null && !sessions[i].isValid()) { expireHere++; } } long timeEnd = System.currentTimeMillis(); if(log.isDebugEnabled()) log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere); processingTime += ( timeEnd - timeNow ); }
最後一個關於逾時的話題也很有意思,在linux中c函數的api的connect方法是沒有提供逾時參數的,如下:
int connect (int sockfd,struct sockaddr * serv_addr,int addrlen);
那麼java是如何通過native方法的封裝提供出帶參數的connect api呢。其實這裡思路與之前是一致的,即尋找一個替代的有逾時參數的方法。首先是設定connect為非阻塞,然後調用connect並立即返回,最後將寫事件註冊到帶逾時的poll。
/* * To do a timed connect we make the socket non-blocking * and poll with a timeout; */ if (attachTimeout > 0) { dbgsysConfigureBlocking(socketFD, JNI_FALSE); } err = dbgsysConnect(socketFD, (struct sockaddr *)&sa, sizeof(sa)); if (err == DBG_EINPROGRESS && attachTimeout > 0) { err = dbgsysFinishConnect(socketFD, (long)attachTimeout); if (err == DBG_ETIMEOUT) { dbgsysConfigureBlocking(socketFD, JNI_TRUE); RETURN_ERROR(JDWPTRANSPORT_ERROR_TIMEOUT, "connect timed out"); } }int dbgsysFinishConnect(int fd, long timeout) { int rv = dbgsysPoll(fd, 0, 1, timeout); if (rv == 0) { return DBG_ETIMEOUT; } if (rv > 0) { return 0; } return rv;}int dbgsysPoll(int fd, jboolean rd, jboolean wr, long timeout) { struct pollfd fds[1]; int rv; fds[0].fd = fd; fds[0].events = 0; if (rd) { fds[0].events |= POLLIN; } if (wr) { fds[0].events |= POLLOUT; } fds[0].revents = 0; rv = poll(&fds[0], 1, timeout); if (rv >= 0) { rv = 0; if (fds[0].revents & POLLIN) { rv |= DBG_POLLIN; } if (fds[0].revents & POLLOUT) { rv |= DBG_POLLOUT; } } return rv;}
參考文章:
http://blog.donews.com/maverick/archive/2005/08/10/502286.aspx
http://my.oschina.net/astute/blog/92339