《FilthyRichClients》讀書筆記(一)-SwingのEDT
《FilthyRichClients》讀完了前幾個(gè)章節(jié),現(xiàn)將我的體會(huì)結(jié)合工作以來(lái)從事Swing桌面開發(fā)的經(jīng)驗(yàn),對(duì)本書的一些重要概念進(jìn)行一次分析,對(duì)書中的一些遺漏與模糊的地方及時(shí)補(bǔ)充,同時(shí)使讀者消除長(zhǎng)期以來(lái)“Swing性能低、界面丑陋”諸如此類的舊觀念。讀書筆記僅談?wù)勎覍?duì)Swing的理解,難免會(huì)犯錯(cuò)誤,還望廣大讀者指教。
書中第二章-Swing渲染基本原理 中對(duì)Swing的線程做了系統(tǒng)地介紹。相比其他同類Swing教程,已經(jīng)講得非常深入了。但是如果讀者之前對(duì)線程的掌握程度有限,尤其是編寫代碼比較隨意的coder們,動(dòng)輒就大量編寫類似下面這樣的代碼:
jButton1.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
// TODO
}
});
這樣的代碼可能是netBeans這樣的工具生成的“杰作”。但是如果這個(gè)人再懶惰一點(diǎn),可能會(huì)直接在TODO下面寫上長(zhǎng)長(zhǎng)一堆代碼,還伴隨著不可預(yù)知的I/O操作,很多人指責(zé)界面被僵住是Swing性能的問(wèn)題。在新式的JDK中,Swing已經(jīng)在性能方面改進(jìn)了很多,完全可以這么說(shuō):與應(yīng)用程序自身的業(yè)務(wù)計(jì)算相比,界面上的耗時(shí)可以忽略。但是如果上述惡習(xí)改不掉的話,Swing永遠(yuǎn)“快”不起來(lái),SWT也同樣如此,因?yàn)樗鼈兌际菃尉€程圖形工具包。
書上有這樣一段話:“EventQueue的派發(fā)機(jī)制由單獨(dú)的一個(gè)線程管理,這個(gè)線程稱為事件派發(fā)線程(EDT)”。和其他很多桌面API一樣,Swing將GUI請(qǐng)求放入一個(gè)事件隊(duì)列中執(zhí)行。如果不明白什么是事件隊(duì)列、EDT,它們是如何運(yùn)作的,那么首先必須澄清四個(gè)重要的概念:分別是同步與異步、串行與并行、生產(chǎn)者消費(fèi)者模式、事件隊(duì)列。(不同領(lǐng)域串行與并行的含義可能是不同的)
同步與異步:同步是程序在發(fā)起請(qǐng)求后開始處理事件并等待處理的結(jié)果或等待請(qǐng)求執(zhí)行完畢,在此之前程序被block住直到請(qǐng)求完成。而異步是當(dāng)前程序發(fā)起請(qǐng)求后立即返回,當(dāng)前程序不會(huì)立即處理該事件并等待處理的結(jié)果,請(qǐng)求是在稍后的某一時(shí)間才被處理。
串行與并行:所謂串行是指多個(gè)要處理請(qǐng)求順序執(zhí)行,處理完一個(gè)再處理下一個(gè);并行可以理解為并發(fā),是同時(shí)處理多個(gè)請(qǐng)求(實(shí)際上我們只能理解為是這樣,特別是CPU數(shù)目少于線程數(shù)的機(jī)器而言,真正意義的并發(fā)是不存在的,各個(gè)線程只是斷斷續(xù)續(xù)地交替地執(zhí)行)。下圖演示了串行與并行的機(jī)制。可以這么說(shuō),在引入多線程之前,對(duì)于同一進(jìn)程或者程序而言執(zhí)行的都是串行操作。
串行:
并行:
生產(chǎn)者/消費(fèi)者模式:可以想象這樣一副場(chǎng)景,某車間的一條傳送帶,有一個(gè)或多個(gè)入口不斷產(chǎn)生待加工的貨物,這種不斷產(chǎn)生貨物的稱為生產(chǎn)者;傳送帶的末端是一個(gè)或多個(gè)工人在加工貨物,稱作消費(fèi)者。有時(shí)由于傳送帶上沒(méi)有足夠的貨物使得某一工人暫時(shí)空閑,有時(shí)又由于部分貨物需加工的時(shí)間較長(zhǎng)出現(xiàn)傳送帶上待加工的貨物堆積。
如果用Java實(shí)現(xiàn)一個(gè)簡(jiǎn)單的生產(chǎn)者消費(fèi)者模型,利用線程的等待/通知機(jī)制很容易實(shí)現(xiàn)。給出最基本的同步隊(duì)列的參考實(shí)現(xiàn)
public class SyncQueue<T> {
private List<T> queue;
private final Object LOCK = new Object();
public SyncQueue() {
queue = new LinkedList<T>();
}
public T pop() throws InterruptedException {
synchronized (LOCK) {
while (queue.isEmpty()) {
try {
LOCK.wait();
} catch (InterruptedException ex) {
throw ex;
}
}
T e = queue.remove(0);
return e;
}
}
public void push(T e) {
synchronized (LOCK) {
queue.add(e);
LOCK.notifyAll();
}
}
}
在JDK 5中新出現(xiàn)了許多具有并發(fā)性的數(shù)據(jù)結(jié)構(gòu)在java.util.concurrent包中,它們適合于特殊的場(chǎng)合,本帖不作解釋。
事件隊(duì)列:在計(jì)算機(jī)數(shù)據(jù)結(jié)構(gòu)中,隊(duì)列是一個(gè)特殊的數(shù)據(jù)結(jié)構(gòu)。其一、它是線性的;其二、元素是先進(jìn)先出的,也就是說(shuō)進(jìn)入隊(duì)列的元素必須從末端進(jìn)入,先入隊(duì)的元素先得到執(zhí)行,后入隊(duì)的元素等待前面的元素執(zhí)行完畢出隊(duì)后才能執(zhí)行,隊(duì)列的處理方式是執(zhí)行完一個(gè)再執(zhí)行下一個(gè)。隊(duì)列與線程安全是兩個(gè)不同的概念,如果要將隊(duì)列加上線程安全的特性,只需要仿照上述生產(chǎn)者/消費(fèi)者加上線程的等待/通知即可。
而Swing的事件隊(duì)列就類似(基本原理相似,但是Swing內(nèi)部實(shí)現(xiàn)會(huì)做些優(yōu)化)于上述的事件隊(duì)列,說(shuō)它是單線程圖形工具包指的是僅有單一消費(fèi)者,也就是常說(shuō)的事件分發(fā)線程(EDT),一般來(lái)講,除非你的應(yīng)用程序停止,否則EDT會(huì)永不間斷地徘徊在處理請(qǐng)求與等待請(qǐng)求之間。下圖是Swing事件隊(duì)列的實(shí)現(xiàn)機(jī)制:
很顯然,如果在加工某一個(gè)貨物上花費(fèi)很長(zhǎng)的時(shí)間,那么后續(xù)的貨物只好等待。對(duì)于單一線程的事件隊(duì)列來(lái)說(shuō)有兩個(gè)非常突出的特性:一、將同步操作轉(zhuǎn)為異步操作。二、將并行處理轉(zhuǎn)換為串行順序處理。
如果你能理解上述圖,那么你就應(yīng)該意識(shí)到:EDT要處理所有GUI操作,它是職責(zé)分明且非常忙碌的。也就是說(shuō)你要記住兩條原則:一、職責(zé)分明,任何GUI請(qǐng)求都應(yīng)該在EDT中調(diào)用。二、需要處理的GUI請(qǐng)求非常多,包括窗口移動(dòng)、組件自動(dòng)重繪、刷新,它很忙,所以任何與GUI無(wú)關(guān)的處理不要由EDT來(lái)負(fù)責(zé),尤其是I/O這種耗時(shí)的操作。
書中還講到Swing不是一個(gè)“安全線程”的API,為什么要這樣設(shè)計(jì),再回看上圖就會(huì)明白:Swing的線程安全不是靠自身組件的API來(lái)保障,雖然repaint方法是這樣,但是大多數(shù)Swing API是非線程安全的,也就是說(shuō)不能在任意地方調(diào)用,它應(yīng)該只在EDT中調(diào)用。Swing的線程安全靠事件隊(duì)列和EDT來(lái)保障。
invokeLater和invokeAndWait:前文提到,Swing自身不是線程安全,對(duì)非EDT的并發(fā)調(diào)用需通過(guò)invokeLater(runnable)和invokeAndWait(runnable)使請(qǐng)求插入到隊(duì)列中等待EDT去執(zhí)行。invokeLater(runnable)方法是異步的,它會(huì)立即返回,具體何時(shí)執(zhí)行請(qǐng)求并不確定,所以命名invokeLater是稍后調(diào)用。invokeAndWait(runnable)方法是同步的,它被調(diào)用結(jié)束會(huì)立即block當(dāng)前線程(調(diào)用invokeAndWait的那個(gè)線程)直到EDT處理完那個(gè)請(qǐng)求。invokeAndWait一般的應(yīng)用是取得Swing組件的數(shù)據(jù),例如取得JSlider組件的當(dāng)前值:
public class Task implements Runnable {
private JSlider slider;
private int value;
public Task() {
//slider = ...;
}
@Override
public void run() {
try {
Thread.sleep(1000); // 有意停住1秒
} catch (InterruptedException e) {
}
value = slider.getValue();
}
public int getValue() {
return value;
}
}
而外部非EDT線程可以這樣調(diào)用:
Task task = new Task();
try {
EventQueue.invokeAndWait(task);
} catch (InterruptedException e) {
} catch (InvocationTargetException e) {
}
int value = task.getValue();
當(dāng)線程運(yùn)行到EventQueue.invokeAndWait(task)時(shí)會(huì)立即被block至少1秒,待invokeAndWait返回時(shí)已經(jīng)可以安全地取到值了。invokeAndWait被這樣命名也反映了使用的意圖:調(diào)用并等待結(jié)果。invokeAndWait有非常重要的一條準(zhǔn)則是它不能在EDT中被調(diào)用,否則程序會(huì)拋出Error,請(qǐng)求也不會(huì)去執(zhí)行。
public static void invokeAndWait(Runnable runnable)
throws InterruptedException, InvocationTargetException {
if (EventQueue.isDispatchThread()) {
throw new Error("Cannot call invokeAndWait from the event dispatcher thread");
}
class AWTInvocationLock {} // 聲明這個(gè)類只是鎖的標(biāo)志,沒(méi)有其他意義
Object lock = new AWTInvocationLock();
InvocationEvent event =
new InvocationEvent(Toolkit.getDefaultToolkit(), runnable, lock,
true);
synchronized (lock) {
Toolkit.getEventQueue().postEvent(event); //添加進(jìn)事件隊(duì)列
lock.wait(); // block當(dāng)前線程
}
Throwable eventThrowable = event.getThrowable();
if (eventThrowable != null) {
throw new InvocationTargetException(eventThrowable);
}
}
為什么要有這樣一條限制?結(jié)合前文不難得出-防止死鎖。如果invokeAndWait在EDT中調(diào)用,那么首先將請(qǐng)求壓進(jìn)隊(duì)列,然后EDT便被block(因?yàn)樗褪钦{(diào)用invokeAndWait的當(dāng)前線程)等待請(qǐng)求結(jié)束通知它繼續(xù)運(yùn)行,而實(shí)際上請(qǐng)求將永遠(yuǎn)得不到執(zhí)行,因?yàn)樗诘却?duì)列的調(diào)度使EDT執(zhí)行它,這就陷入一個(gè)僵局-EDT等待請(qǐng)求先執(zhí)行,請(qǐng)求又等待EDT對(duì)隊(duì)列的調(diào)度。彼此等待對(duì)方釋放鎖是造成死鎖的四類條件之一。Swing有意地避免了這類情況的發(fā)生。
書中也提到了同步的繪制請(qǐng)求,作為隊(duì)列,一條基本原則就是先進(jìn)先出。那么paintImmediately到底是怎樣的呢?顯然這個(gè)調(diào)用請(qǐng)求不會(huì)稍后去執(zhí)行,也就是說(shuō)不會(huì)插入到隊(duì)列的末尾等到排在它前面的請(qǐng)求執(zhí)行完再去執(zhí)行它,而是“破壞”順序性原則優(yōu)先去執(zhí)行,前面提到,Swing的事件隊(duì)列相對(duì)基礎(chǔ)的同步隊(duì)列做了很多優(yōu)化,那么這么說(shuō)它是否被插入到隊(duì)列最前面呢,也就是0這個(gè)位置?貌似也不是,書上說(shuō)“已經(jīng)在EDT中調(diào)用的方法中間...”,那么就是比當(dāng)前正在處理的繪制請(qǐng)求還要優(yōu)先,因?yàn)樗钱?dāng)前繪制請(qǐng)求的一部分,所以當(dāng)前繪制請(qǐng)求(EDT正在處理的那個(gè)請(qǐng)求)要等它處理完成后再繼續(xù)處理。(好好體會(huì)吧)
SwingWorker:推薦一篇Blog,http://blog.sina.com.cn/s/blog_4b6047bc010007so.html,作者是原Sun中國(guó)工程研究院的陳維雷先生,他對(duì)Swing的造詣非淺,他的Blog中有3篇介紹這一主題的文章,詳盡程度要比該書詳細(xì)得多。
最后,談一下理解EDT對(duì)設(shè)計(jì)模式的幫助。通過(guò)上述對(duì)事件隊(duì)列和EDT的分析,有這樣一種體會(huì):事件隊(duì)列是一個(gè)非常好的處理并發(fā)設(shè)計(jì)模型,不僅Swing用它來(lái)處理后臺(tái),Java的很多地方都在用,只不過(guò)對(duì)于處理服務(wù)器端的并發(fā)請(qǐng)求有多個(gè)處理線程在等候處理請(qǐng)求,也就是常說(shuō)的線程池。而對(duì)于單用戶的桌面應(yīng)用,單線程調(diào)用要比多現(xiàn)成API更簡(jiǎn)單,“Swing后臺(tái)這樣做是為了保證事件的順序和可預(yù)見(jiàn)性”,而且相對(duì)于服務(wù)器,客戶端桌面層的請(qǐng)求要少得多,所以單線程就足夠應(yīng)對(duì)了。
單一Thread化的訪問(wèn):
通過(guò)EDT,使得不具備線程安全的Swing函數(shù)庫(kù)避開了并發(fā)訪問(wèn)的問(wèn)題。如果你也有一個(gè)不具備thread安全性的函數(shù)庫(kù)并想在multithreaded環(huán)境下使用應(yīng)該怎么辦?只要你是從單一的thread來(lái)訪問(wèn)這個(gè)函數(shù)庫(kù),程序就不會(huì)遭遇到任何數(shù)據(jù)同步的問(wèn)題。
posted on 2008-06-23 22:49 sun_java_studio@yahoo.com.cn(電玩) 閱讀(10873) 評(píng)論(7) 編輯 收藏 所屬分類: NetBeans