Jigsaw 中線程池的設計對我的觸發很大,因爲之前沒有涉及到這方面的項目,多線程以及線程池之類感念還比較神祕,通過對 jigsaw 的學習,這層神祕面紗也被褪去,同時很是感慨大師們的設計之妙。現在提一個問題,如何設計處理 http 請求的程序?如果我沒有看過 jigsaw ,我是無從下手的,當然,從以往的學習中,只能從概念上提出線程池的解決方案,但具體怎麼做,如何操作,我還是沒有什麼頭緒。
當啓動一個 http 服務器後,客戶端會有多少請求?什麼時候到?怎麼樣儘快處理每個請求,而不讓用戶長時間等待?這些問題只有多線程才能解決,那麼在服務器啓動時,可以啓動指定數量的線程放在一個池中,每個線程都有不同的狀態,當有請求過來時,便從池中取得一個空閒線程來處理這個請求,並改變這個線程的狀態,處理完後,再把線程放回池中,當有其他請求過來時,再從池中取得線程,這樣可以循環利用所有線程,實現資源的最大利用率。下面就來仔細看看 jigsaw 的多線程設計。
對於每個 http 請求,可以抽象爲一個客戶對象,用來封裝 http 請求,在客戶工廠類中就需要定義放置這些客戶對象以及處理這些客戶對象的線程的容器, jigsaw 使用了鏈表來保存這些客戶對象,分別構成了客戶池和線程池。一個新請求到達,就從客戶池請求一個客戶對象封裝這個請求,並從線程池請求一個空閒線程,把這個客戶對象交給這個線程處理,而不會影響其他的請求。
如何設計這個客戶池呢? jigsaw 提供了一個抽象類 LRUList ,它包含兩個屬性 :
protected LRUNode head ;// 記錄這個鏈表的表頭節點
protected LRUNode tail ;// 記錄這個鏈表的表尾節點
LRUNode 類就是客戶池鏈表的節點類型,它實現了 LRUAble 接口,幷包含它前一個節點和後一個節點的引用,這是 LRUNode 類的屬性 :
protected LRUAble prev ;// 前一個節點的引用
protected LRUAble next ;// 後一個節點的引用
在客戶工廠類中的客戶池實際上是一個繼承了 LRUList 的類 SyncLRUList ,因爲後面對客戶池的操作是在多線程環境下,該類中實現的抽象方法都是同步的。從客戶池中獲取一個客戶對象是調用鏈表的 removeTail() 方法,下面是這個方法以及相關代碼 :
public final synchronized LRUAble removeTail() {
// 如果鏈表的尾節點的前節點不是鏈表的頭節點,則移除該尾節點的前節點,並將移除的節點返回給調用者
if ( tail . prev != head )
return remove (tail.prev) ;
return null;
}
public final synchronized LRUAble remove(LRUAble node) {
_remove(node) ;
node.setNext((LRUAble) null ) ;
node.setPrev((LRUAble) null ) ;
return node ;
}
private final synchronized void _remove(LRUAble node) {
LRUAble itsPrev, itsNext ;
itsPrev = node.getPrev() ;
// note assumption: if its prev is not null, neither is its next
if (itsPrev== null ) return ;
itsNext = node.getNext() ;
itsPrev.setNext(itsNext) ;
itsNext.setPrev(itsPrev) ;
}
由上面可知,每次從客戶池取客戶對象,都是取的表尾節點的上一個節點(如果不是表頭節點),然後就可以把客戶套接字綁定到該客戶對象上,處理 http 請求
那麼用於處理客戶對象的線程池又是如何設計的呢?
Jigsaw 提供了 ThreadCache 類,代表了這個線程池,它也有兩個屬性 :
protected CachedThread freehead = nul l; // 被緩存的線程,是鏈表的表頭
protected CachedThread freetail = null ; // 被緩存的線程,是鏈表的表尾
在客戶工廠類的初始化過程中,便對線程池的池大小進行了設置,並初始化了這個線程池,啓動了指定個數的線程放置在池中(實質上也是一個鏈表),最初都處於阻塞狀態。
CachedThread 是一個繼承了 Thread 的類,它的設計思想也和前面 LRUNode 一樣,作爲一個鏈表的節點,都需要保存上一個節點和後一個節點的引用,請看代碼 :
CachedThread next = null ; // 當前線程在鏈表中的下一個線程的引用
CachedThread prev = null ; // 當前線程在鏈表中的上一個線程的引用
另外,它還包含一個屬性 :
Runnable runner = null;
這就是需要交給線程處理的客戶對象,因爲客戶對象實現了 Runnable 接口,從客戶池中取得的客戶對象就可以賦給 runner ,由線程去調用 runner 的 run() 方法處理請求
另外,它包含了其他幾個重要的標誌
boolean alive = true; // 該線程是否活動
boolean terminated = false; // 該線程處理客戶對象是否終止
boolean started = false ; // 該線程是否已啓動
boolean firstime = true ; // 該線程是否第一次啓動
這是 CachedThread 的 run() 方法,最開始在調用 waitForRunner() 會被阻塞,直到該線程獲得一個客戶對象
public void run() {
try {
while ( true ) {
// Wait for a runner:
Runnable torun = waitForRunner();
// If runner, run:
if ( torun != null )
torun.run(); //torun 實際上是一個 SocketClient 對象
// If dead, stop
if ( ! alive )
break ;
}
} finally {
cache .isDead( this );
}
}
waitForRunner() 方法首先會判斷 runner 屬性是否爲空,不爲空就會把它返回給調用者,返回之前會把 runner 重新置爲 null ;其次判斷該線程是否爲第一次啓動,是第一次就將線程阻塞;最後判斷當前線程對象是否是活動的,是活動的就阻塞它。
synchronized Runnable waitForRunner() {
boolean to = false ;
while ( alive ) {
// Is a runner available ?
if ( runner != null ) {
Runnable torun = runner ;
firstime = false ;
runner = null ;
return torun;
} else if ( firstime ) {
// This thread will not be declared free until it runs once:
try {
wait();
} catch (InterruptedException ex) {}
} else if ( alive = cache .isFree( this , to) ) {
// Notify the cache that we are free, and continue if allowed:
try {
int idleto = cache .getIdleTimeout();
to = false ;
if ( idleto > 0 ) {
wait(idleto);
to = ( runner == null );
} else {
wait();
}
} catch (InterruptedException ex) {}
}
}
return null ;
}
Wakeup 方法用於喚醒當前阻塞的線程
synchronized boolean wakeup(Runnable runnable) {
if ( alive ) {
runner = runnable;
if ( ! started )
this .start();
notify();
return true ;
} else {
return false ;
}
}