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