JAVA后臺程序設計及UTIL.CONCURRENT包的應用
JAVA后臺程序設計及UTIL.CONCURRENT包的應用
何 恐
摘要 : 在很多軟件項目中,JAVA語言常常被用來開發后臺服務程序。線程池技術是提高這類程序性能的一個重要手段。在實踐中,該技術已經被廣泛的使用。本文首先 對設計后臺服務程序通常需要考慮的問題進行了基本的論述,隨后介紹了JAVA線程池的原理、使用和其他一些相關問題,最后對功能強大的JAVA開放源碼線 程池包util.concurrent 在實際編程中的應用進行了詳細介紹。
關鍵字: JAVA;線程池;后臺服務程序;util.concurrent
1 引言
在軟件項目開發中,許多后臺服務程序的處理動作流程都具有一個相同點,就是:接受客戶端發來的請求,對請求進行一些相關的處理,最后將處理結果返回給客戶 端。這些請求的來源和方式可能會各不相同,但是它們常常都有一個共同點:數量巨大,處理時間短。這類服務器在實際應用中具有較大的普遍性,如web服務 器,短信服務器,DNS服務器等等。因此,研究如何提高此類后臺程序的性能,如何保證服務器的穩定性以及安全性都具有重要的實用價值。
2 后臺服務程序設計
2.1 關于設計原型
構建服務器應用程序的一個簡單的模型是:啟動一個無限循環,循環里放一個監聽線程監聽某個地址端口。每當一個請求到達就創建一個新線程,然后新線程為請求服務,監聽線程返回繼續監聽。
簡單舉例如下:
import java.net.*;
public class MyServer extends Thread{
public void run(){
try{
ServerSocket server=null;
Socket clientconnection=null;
server = new ServerSocket(8008);//監聽某地址端口對
while(true){進入無限循環
clientconnection =server.accept();//收取請求
new ServeRequest(clientconnection).start();//啟動一個新服務線程進行服務
……
}
}catch(Exception e){
System.err.println("Unable to start serve listen:"+e.getMessage());
e.printStackTrace();
}
}
}
實際上,這只是個簡單的原型,如果試圖部署以這種方式運行的服務器應用程序,那么這種方法的嚴重不足就很明顯。
首先,為每個請求創建一個新線程的開銷很大,為每個請求創建新線程的服務器在創建和銷毀線程上花費的時間和消耗的系統資源, 往往有時候要比花在處理實際的用戶請求的時間和資源更多。在Java中更是如此,虛擬機將試圖跟蹤每一個對象,以便能夠在對象銷毀后進行垃圾回收。所以提 高服務程序效率的一個手段就是盡可能減少創建和銷毀對象的次數。這樣綜合看來,系統的性能瓶頸就在于線程的創建開銷。
其次,除了創建和銷毀線程的開銷之外,活動的線程也消耗系統資源。在一個 JVM 里創建太多的線程可能會導致系統由于過度消耗內存而用完內存或“切換過度”。為了防止資源不足,服務器應用程序需要一些辦法來限制任何給定時刻運行的處理 線程數目,以防止服務器被“壓死”的情況發生。所以在設計后臺程序的時候,一般需要提前根據服務器的內存、CPU等硬件情況設定一個線程數量的上限值。
如果創建和銷毀線程的時間相對于服務時間占用的比例較大,那末假設在一個較短的時間內有成千上萬的請求到達,想象一下,服務器的時間和資源將會大量的花在 創建和銷毀線程上,而真正用于處理請求的時間卻相對較少,這種情況下,服務器性能瓶頸就在于創建和銷毀線程的時間。按照這個模型寫一個簡單的程序測試一下 即可看出,由于篇幅關系,此處略。如果把(服務時間/創建和銷毀線程的時間)作為衡量服務器性能的一個參數,那末這個比值越大,服務器的性能就越高。
應此,解決此類問題的實質就是盡量減少創建和銷毀線程的時間,把服務器的資源盡可能多地用到處理請求上來,從而發揮多線程的優點(并發),避免多線程的缺點(創建和銷毀的時空開銷)。
線程池為線程生命周期開銷問題和資源不足問題提供了解決方案。通過對多個任務重用線程,線程創建的開銷被分攤到了多個任務上。其好處是,因為在請求到達時 線程已經存在,所以無意中也消除了線程創建所帶來的延遲。這樣,就可以立即為請求服務,使應用程序響應更快。而且,通過適當地調整線程池中的線程數目,也 就是當請求的數目超過某個閾值時,就強制其它任何新到的請求一直等待,直到獲得一個線程來處理為止,從而可以防止資源不足。
3 JAVA線程池原理
3.1 原理以及實現
在實踐中,關于線程池的實現常常有不同的方法,但是它們的基本思路大都是相似的:服務器預先存放一定數目的“熱”的線程,并發程序需要使用線程的時候,從 服務器取用一條已經創建好的線程(如果線程池為空則等待),使用該線程對請求服務,使用結束后,該線程并不刪除,而是返回線程池中,以備復用,這樣可以避 免對每一個請求都生成和刪除線程的昂貴操作。
一個比較簡單的線程池至少應包含線程池管理器、工作線程、任務隊列、任務接口等部分。其中線程池管理器(ThreadPool Manager)的作用是創建、銷毀并管理線程池,將工作線程放入線程池中;工作線程是一個可以循環執行任務的線程,在沒有任務時進行等待;任務隊列的作 用是提供一種緩沖機制,將沒有處理的任務放在任務隊列中;任務接口是每個任務必須實現的接口,主要用來規定任務的入口、任務執行完后的收尾工作、任務的執 行狀態等,工作線程通過該接口調度任務的執行。下面的代碼實現了創建一個線程池:
public class ThreadPool
{
private Stack threadpool = new Stack();
private int poolSize;
private int currSize=0;
public void setSize(int n)
{
poolSize = n;
}
public void run()
{
for(int i=0;i
(發帖時間:2003-11-30 11:55:56)
--- 岑心 J
回復(1):
4.2 框架與結構
下面讓我們來看看util.concurrent的框架結構。關于這個工具包概述的e文原版鏈接地址是http: //gee.cs.oswego.edu/dl/cpjslides/util.pdf。該工具包主要包括三大部分:同步、通道和線程池執行器。第一部分 主要是用來定制鎖,資源管理,其他的同步用途;通道則主要是為緩沖和隊列服務的;線程池執行器則提供了一組完善的復雜的線程池實現。
--主要的結構如下圖所示
4.2.1 Sync
acquire/release協議的主要接口
- 用來定制鎖,資源管理,其他的同步用途
- 高層抽象接口
- 沒有區分不同的加鎖用法
實現
-Mutex, ReentrantLock, Latch, CountDown,Semaphore, WaiterPreferenceSemaphore, FIFOSemaphore, PrioritySemaphore
還有,有幾個簡單的實現,例如ObservableSync, LayeredSync
舉例:如果我們要在程序中獲得一獨占鎖,可以用如下簡單方式:
try {
lock.acquire();
try {
action();
}
finally {
lock.release();
}
}catch(Exception e){
}
程序中,使用lock對象的acquire()方法獲得一獨占鎖,然后執行您的操作,鎖用完后,使用release()方法釋放之即可。呵呵,簡單吧,想 想看,如果您親自撰寫獨占鎖,大概會考慮到哪些問題?如果關鍵的鎖得不到怎末辦?用起來是不是會復雜很多?而現在,以往的很多細節和特殊異常情況在這里都 無需多考慮,您盡可以把精力花在解決您的應用問題上去。
4.2.2 通道(Channel)
為緩沖,隊列等服務的主接口
具體實現
LinkedQueue, BoundedLinkedQueue,BoundedBuffer, BoundedPriorityQueue, SynchronousChannel, Slot
通道例子
class Service { // ...
final Channel msgQ = new LinkedQueue();
public void serve() throws InterruptedException {
String status = doService();
msgQ.put(status);
}
public Service() { // start background thread
Runnable logger = new Runnable() {
public void run() {
try {
for(;;)
System.out.println(msqQ.take());
}
catch(InterruptedException ie) {} }
};
new Thread(logger).start();
}
}
在后臺服務器中,緩沖和隊列都是最常用到的。試想,如果對所有遠端的請求不排個隊列,讓它們一擁而上的去爭奪cpu、內存、資源,那服務器瞬間不當掉才怪。而在這里,成熟的隊列和緩沖實現已經提供,您只需要對其進行正確初始化并使用即可,大大縮短了開發時間。
4.2.3執行器(Executor)
Executor是這里最重要、也是我們往往最終寫程序要用到的,下面重點對其進行介紹。
類似線程的類的主接口
- 線程池
- 輕量級運行框架
- 可以定制調度算法
只需要支持execute(Runnable r)
- 同Thread.start類似
實現
- PooledExecutor, ThreadedExecutor, QueuedExecutor, FJTaskRunnerGroup
PooledExecutor(線程池執行器)是個最常用到的類,以它為例:
可修改得屬性如下:
- 任務隊列的類型
- 最大線程數
- 最小線程數
- 預熱(預分配)和立即(分配)線程
- 保持活躍直到工作線程結束
-- 以后如果需要可能被一個新的代替
- 飽和(Saturation)協議
-- 阻塞,丟棄,生產者運行,等等
可不要小看上面這數條屬性,對這些屬性的設置完全可以等同于您自己撰寫的線程池的成百上千行代碼。下面以筆者撰寫過得一個GIS服務器為例:
該GIS服務器是一個典型的“請求-服務”類型的服務器,遵循后端程序設計的一般框架。首先對所有的請求按照先來先服務排入一個請求隊列,如果瞬間到達的 請求超過了請求隊列的容量,則將溢出的請求轉移至一個臨時隊列。如果臨時隊列也排滿了,則對以后達到的請求給予一個“服務器忙”的提示后將其簡單拋棄。這 個就夠忙活一陣的了。
然后,結合鏈表結構實現一個線程池,給池一個初始容量。如果該池滿,以x2的策略將池的容量動態增加一倍,依此類推,直到總線程數服務達到系統能力上限, 之后線程池容量不在增加,所有請求將等待一個空余的返回線程。每從池中得到一個線程,該線程就開始最請求進行GIS信息的服務,如取坐標、取地圖,等等。 服務完成后,該線程返回線程池繼續為請求隊列離地后續請求服務,周而復始。當時用矢量鏈表來暫存請求,用wait()、 notify() 和 synchronized等原語結合矢量鏈表實現線程池,總共約600行程序,而且在運行時間較長的情況下服務器不穩定,線程池被取用的線程有異常消失的 情況發生。而使用util.concurrent相關類之后,僅用了幾十行程序就完成了相同的工作而且服務器運行穩定,線程池沒有丟失線程的情況發生。由 此可見util.concurrent包極大的提高了開發效率,為項目節省了大量的時間。
使用PooledExecutor例子
import java.net.*;
/**
* Title:
* Description: 負責初始化線程池以及啟動服務器
* Copyright: Copyright (c) 2003
* Company:
* @author not attributable
* @version 1.0
*/
public class MainServer {
//初始化常量
public static final int MAX_CLIENT=100; //系統最大同時服務客戶數
//初始化線程池
public static final PooledExecutor pool =
new PooledExecutor(new BoundedBuffer(10), MAX_CLIENT); //chanel容量為10,
//在這里為線程池初始化了一個
//長度為10的任務緩沖隊列。
public MainServer() {
//設置線程池運行參數
pool.setMinimumPoolSize(5); //設置線程池初始容量為5個線程
pool.discardOldestWhenBlocked();//對于超出隊列的請求,使用了拋棄策略。
pool.createThreads(2); //在線程池啟動的時候,初始化了具有一定生命周期的2個“熱”線程
}
public static void main(String[] args) {
MainServer MainServer1 = new MainServer();
new HTTPListener().start();//啟動服務器監聽和處理線程
new manageServer().start();//啟動管理線程
}
}
類HTTPListener
import java.net.*;
/**
* Title:
* Description: 負責監聽端口以及將任務交給線程池處理
* Copyright: Copyright (c) 2003
* Company:
* @author not attributable
* @version 1.0
*/
public class HTTPListener extends Thread{
public HTTPListener() {
}
public void run(){
try{
ServerSocket server=null;
Socket clientconnection=null;
server = new ServerSocket(8008);//服務套接字監聽某地址端口對
while(true){//無限循環
clientconnection =server.accept();
System.out.println("Client connected in!");
//使用線程池啟動服務
MainServer.pool.execute(new HTTPRequest(clientconnection));//如果收到一個請求,則從線程池中取一個線程進行服務,任務完成后,該線程自動返還線程池
}
}catch(Exception e){
System.err.println("Unable to start serve listen:"+e.getMessage());
e.printStackTrace();
}
}
}
關于util.concurrent工具包就有選擇的介紹到這,更詳細的信息可以閱讀這些java源代碼的API文檔。Doug Lea是個很具有“open”精神的作者,他將util.concurrent工具包的java源代碼全部公布出來,有興趣的讀者可以下載這些源代碼并細 細品味。
5 結束語
以上內容介紹了線程池基本原理以及設計后臺服務程序應考慮到的問題,并結合實例詳細介紹了重要的多線程開發工具包util.concurrent的構架和使用。結合使用已有完善的開發包,后端服務程序的開發周期將大大縮短,同時程序性能也有了保障。
參考文獻
[1] Chad Darby,etc. 《Beginning Java Networking》. 電子工業出版社. 2002年3月.
[2] util.concurrent 說明文件 http://gee.cs.oswego.edu/dl/cpjslides/util.pdf
[3] 幸勇.線程池的介紹及簡單實現.http://www-900.ibm.com/developerWorks/cn/java/l-threadPool/index.shtml
2002年8月
[4]BrianGoetz. 我的線程到哪里去了http://www-900.cn.ibm.com/developerworks/cn/java/j-jtp0924/index.shtml
posted on 2008-09-07 11:10 大石頭 閱讀(380) 評論(0) 編輯 收藏 所屬分類: 多線程