本文分析了Eclipse中多線程程序的實現討論了在Eclipse客戶端程序開發中應用多線程的方法和要注意的問題同時也討論了多線程程序的一些調試和問題解決的方法
Eclipse作為一個開發平台使用越來越廣泛基於Eclipse Rich Client Platform開發的客戶端程序也越來越多在當今越來越復雜的應用環境中我們的客戶端程序不可避免的要同時進行多任務的處理一個優異的客戶端程序都會允許用戶同時啟動多個任務從而大大提高用戶的工作效率以及用戶體驗本文中我們來談談Eclipse中實現多任務的方式
在我們基於Eclipse的Java程序中我們有很多種方式提供多任務的實現熟悉Java的朋友立即會想到Java的Thread類這是Java中使用最多的一個實現多任務的類Eclipse平台為多任務處理提供了自己的API那就是Job以及UIJobEclipse中的Job是對Java Thread的一個封裝為我們實現多任務提供了更方便的接口以下是Job的基本用法
清單 Job用法示例
Job job = new Job(Job Name){
protected IStatus run(IProgressMonitor monitor) {
// 在這裡添加你的任務代碼
return StatusOK_STATUS;
}
};
jobschedule(delayTime);
在Eclipse中我們也會經常用到DisplayasynchExec() 和DisplaysynchExec()來啟動任務的執行這兩個方法主要為了方便我們完成界面操作的任務以下是DisplayasynchExec()的用法DisplaysynchExec()和它類似
清單 DisplaysynchExec()用法示例
DisplaygetDefault()asyncExec(new Runnable() {
public void run() {
// 在這裡添加你的任務代碼
}
});
通常在Eclipse中我們最好使用Eclipse提供的Job接口來實現多任務而不是使用Java的thread為什麼呢?主要有以下幾個原因
Job是可重用的工作單元
一個Job我們可以很方便的讓它多次執行
Job提供了方便的接口
使得我們在處理中能夠很方便的與外界交流
報告當前的執行進度
Eclipse提供了相應的機制使得程序員可以方便的介入Job的調度
例如我們可以方便的實現每次只有一個同一類型的Job在運行
Eclipse缺省提供了Job管理的程序
可以查看當前所有的Job和它們的進度
也提供UI終止
暫停
繼續指定的Job
使用Job可以提高程序的性能
節省線程創建和銷毀的開銷
Eclipse中的Job封裝了線程池的實現
當我們啟動一個Job時
Eclipse不會馬上新建一個Thread
它會在它的線程池中尋找是否有空閒的線程
如果有空閒線程
就會直接用空閒線程運行你的Job
一個Job終止時
它所對應的線程也不會立即終止
它會被返回到線程池中以備重復利用
這樣
我們可以節省創建和銷毀線程的開銷
下面我們從幾個方面來討論Eclipse中Job的實現和使用方面的問題
Eclipse中Job的實現
Eclipse的核心包中提供了一個JobManager類它實現了IJobManager接口Eclipse中Job的管理和調度都是由JobManager來實現的 JobManager維護有一個線程池用來運行Job當我們調用Job的schedule方法後這個Job會被JobManager首先放到一個Job運行的等待隊列中去之後JobManager會通知線程池有新的Job加入了運行等待隊列線程池會找出一個空閒的線程來運行Job如果沒有空閒線程線程池會創建一個新的線程來運行Job一旦Job運行完畢運行Job的線程會返回到線程池中以備下次使用從上面Job運行的過程我們可以看到JobManager介入了一個Job運行的全過程它了解Job什麼時候開始什麼時候結束每一時候Job的運行狀態JobManager將這些Job運行的信息以接口的方式提供給用戶同時它也提供了接口讓我們可以介入Job的調度等從而我們擁有了更加強大的控制Job的能力
為了我們更方便的了解Job所處的狀態JobManager設置Job的一個狀態標志位我們可以通過Job的getState方法獲得Job當前的狀態值以了解其狀態
NONE當一個Job剛構造的時候Job就會處於這種狀態當一個Job執行完畢(包括被取消)後Job的狀態也會變回這種狀態
WAITING:當我們調用了Job的shedule方法JobManager會將Job放入等待運行的Job隊列這時Job的狀態為WAITING
RUNNING:當一個Job開始執行Job的狀態會變為RUNNING
SLEEPING:當我們調用Job的sleep方法後Job會變成這一狀態當我們調用schudule方法的時候帶上延時的參數Job的狀態也會轉入這一狀態在這一段延時等待的時間中Job都處於這一狀態這是一種睡眠狀態Job在這種狀態中時不能馬上轉入運行我們可以調用Job的wakeup方法來將Job喚醒這樣Job又會轉入WAITING狀態等待運行
Eclipse中的UI線程
另外在Eclipse的線程處理中有一個UI線程的概念Eclipse程序中的主線程是一個特殊的線程程序啟動後會先執行這個線程也就是我們的main()函數所在的線程作為桌面應用程序我們的主線程主要負責界面的響應以及繪制界面元素所以通常我們也叫它UI線程
以下代碼編過SWT應用程序的讀者會非常熟悉它一般出現在main函數的結尾下面來仔細分析一下它的詳細情況
//當窗口未釋放時
while (!shellisDisposed()) {
//如果display對象事件隊列中沒有了等待的事件就讓該線程進入等待狀態
if (!displayreadAndDispatch())
displaysleep();
}
上面的程序實際上就是我們UI線程的處理邏輯當程序啟動後UI線程會讀取事件等待隊列看有沒有事件等待處理如果有它會進行相應處理如果沒有它會進入睡眠狀態如果有新的事件到來它又會被喚醒進行處理UI線程所需要處理的事件包括用戶的鼠標和鍵盤操作事件操作系統或程序中發出的繪制事件一般來說處理事件的過程也就是響應用戶操作的過程
一個好的桌面應用程序需要對用戶的操作作出最快的響應也就是說我們的UI線程必須盡快的處理各種事件從我們程序的角度來說在UI線程中我們不能進行大量的計算或者等待否則用戶操作事件得不到及時的處理通常如果有大量的計算或者需要長時間等待(例如進行網絡操作或者數據庫操作)時我們必須將這些長時間處理的程序單獨開辟出一個線程來執行這樣雖然後台運行著程序但也不會影響界面上的操作
除主線程之外的所有線程都是非UI線程在Eclipse程序中我們所有對界面元素的操作都必須放到UI線程中來執行否則會拋出Exception所以我們要區分出UI線程和非UI線程保證我們對UI的操作都在UI線程中執行
如何判斷當前線程是否UI線程: 你可以通過調用DisplaygetCurrent()來知道當前線程是否是UI線程如果DisplaygetCurrent()返回為空表示當前不是UI線程
Eclipse中使用線程的幾種典型情況
控制Job的並發運行
對於某些Job為了避免並發性問題我們希望同時只有一個這樣的Job在運行這時我們需要控制Job的並發運行在另一種情況下我們也需要控制Job的並發運行我們在程序中對於一個任務我們有可能會啟動一個Job來執行對於少量的任務來說這是可行的但是如果我們預測可能會同時有大量的任務如果每一個任務啟動一個Job我們同時啟動的Job就會非常多這些Job會侵占大量的資源影響其他任務的執行我們可以使用Job的rule來實現控制Job的並發執行簡單的我們可以通過下面的代碼實現我們先定義一個如下rule
private ISchedulingRule Schedule_RULE = new ISchedulingRule() {
public boolean contains(ISchedulingRule rule) {
return thisequals(rule);
}
public boolean isConflicting(ISchedulingRule rule) {
return thisequals(rule);
}
};
對於需要避免同時運行的Job我們可以將它們的rule設成上面定義的rule如
myjobsetRule(Schedule_RULE);
myjobsetRule(Schedule_RULE);
這樣對於myjob和myjob這兩個Job它們不會再同時執行Myjob會等待myjob執行完再執行這是由Eclipse的JobManager來提供實現的JobManager可以保證所有啟動的Job中任意兩個Job的rule是沒有沖突的我們在上面定義的rule是最簡單的我們可以重寫isConflicting函數來實現一些更加復雜的控制比如控制同時同類型的Job最多只有指定的個數在運行但是我們要注意isConflicting方法不能過於復雜一旦一個Job的rule與其他Job的rule有沖突isConflicting方法會調用很多次如果其中的計算過於復雜會影響整體的性能
根據需要執行Job
由於我們有的Job有可能不是立即執行的在有些情況下等到該Job准備執行的時候該Job所要執行的任務已經沒有意義了這時我們可以使用Job的shouldSchedule()和shouldRun()來避免Job的運行在我們定義一個Job時我們可以重載shouldSchedule和shouldRun方法在這些方法中我們可以檢查Job運行的一些先決條件如果這些條件不滿足我們就可以返回falseJobManager在安排Job運行時它會先調用該Job的shouldSchedule方法如果返回為falseJobManager就不會再安排這個Job運行了同樣JobManager在真正啟動一個線程運行一個Job前它會調用該Job的shouldRun方法如果返回false它不再運行這個Job在下面的例子中我們希望啟動一個Job在十秒鐘之後更新文本框中的內容為了保證我們的Job運行時是有意義的我們需要確保我們要更新的文本框沒有被銷毀我們重載了shouldSchedule和shouldRun方法
Text text = new Text(parentSWTNONE);
UIJob refreshJob = new UIJob(更新界面){
public IStatus runInUIThread(IProgressMonitor monitor) {
textsetText(新文本);
return StatusOK_STATUS;
}
public boolean shouldSchedule(){
return !textisDisposed();
}
public boolean shouldRun(){
return !textisDisposed();
}
};
refreshJobschedule();
在UI線程中涉及長時間處理的任務
我們經常碰到這樣一種情況用戶操作菜單或者按鈕會觸發查詢大量數據數據查詢完後更新表格等界面元素用戶點擊菜單或者按鈕所觸發的處理程序一般處於UI線程為了避免阻塞UI我們必須把數據查詢等費時的工作放到單獨的Job中執行一旦數據查詢完畢我們又必須更新界面這時我們又需要使用UI線程進行處理下面是處理這種情況的示例代碼
buttonaddSelectionListener(new SelectionListener(){
public void widgetSelected(SelectionEvent e){
perform();
}
public void widgetDefaultSelected(SelectionEvent e){
perform();
}
private void perform(){
Job job = new Job(獲取數據){
protected IStatus run(IProgressMonitor monitor){
// 在此添加獲取數據的代碼
DisplaygetDefault()asyncExec(new Runnable(){
public void run(){
// 在此添加更新界面的代碼
}
});
}
};
jobschedule();
}
});
延時執行Job避免無用的Job運行
我們經常需要根據選中的對象刷新我們部分的界面元素如果我們連續很快的改變選擇而每次刷新界面涉及到的區域比較大時界面會出現閃爍從用戶的角度來說我們很快的改變選擇希望看到的只是最後選中的結果中間的界面刷新都是不必要的
在Jface中StructuredViewer提供了addPostSelectionChangedListener方法如果我們使用這個方法監聽selectionChanged事件當用戶一直按著方向鍵改變選中時我們只會收到一個selectionChanged事件這樣我們可以避免過度的刷新界面
實際上Jface中就是通過延時執行Job來實現這一功能的我們也可以自己實現類似功能
private final static Object UPDATE_UI_JOBFAMILY = new Object();
tableviewer addSelectionChangedListener (new ISelectionChangedListener (){
public void selectionChanged(SelectionChangedEvent event){
JobgetJobManager()cancel(UPDATE_UI_JOBFAMILY);
new UIJob(更新界面) {
protected IStatus runInUIThread (IProgressMonitor monitor) {
//更新界面
return StatusOK_STATUS;
}
public boolean belongsTo(Object family){
return family== UPDATE_UI_JOBFAMILY;
}
}schedule();
}
});
首先我們需要將界面更新的代碼放到一個UIJob中同時我們將Job延時毫秒執行(我們可以根據需要改變延時的時間)如果下一個selectionChanged事件很快到來我們的調用JobgetJobManager()cancel(UPDATE_UI_JOBFAMILY)將以前未運行的Job取消這樣只有最後一個Job會真正運行
在UI線程中等待非UI線程的結束
有時我們在UI線程中需要等待一個非UI線程執行完我們才能繼續執行例如我們在UI線程中要顯示某些數據但是這些數據又需要從數據庫或者遠程網絡獲取於是我們會啟動一個非UI的線程去獲取數據而我們的UI線程必須要等待這個非UI線程執行完成我們才能繼續執行當然一種簡單的實現方法是使用join我們可以在UI線程中調用非UI線程的join方法這樣我們就可以等待它執行完了我們再繼續但是這會有一個問題當我們的UI線程等待時意味著我們的程序不會再響應界面操作也不會刷新這樣用戶會覺得我們的程序象死了一樣沒有反應這時我們可以使用ModalContext類你可以將你要執行的獲取數據的任務用ModalContext的run方法來運行(如下)ModalContext會將你的任務放到一個獨立的非UI線程中執行並且等待它執行完再繼續執行與join方法不同的是ModalContext在等待時不會停止UI事件的處理這樣我們的程序就不會沒有響應了
try {
ModalContextrun(new IRunnableWithProgress(){
public void run(IProgressMonitor monitor)
throws InvocationTargetException InterruptedException {
/*需要在非UI線程中執行的代碼*/
ModalContextcheckCanceled(monitor);
}
} true new NullProgressMonitor() DisplaygetCurrent());
} catch (InvocationTargetException e) {
} catch (InterruptedException e) {
}
針對相關聯的Job統一進行處理
有時我們需要對相關聯的Job一起處理例如需要同時取消這些Job或者等待所有這些Job結束這時我們可以使用Job Family對於相關聯的Job我們可以將它們設置成同一個Job Family我們需要重載Job的belongsTo方法以設置一個Job的Job Family
Private Object MY_JOB_FAMILY = new Object();
Job job = new Job(Job Name){
protected IStatus run(IProgressMonitor monitor) {
// 在這裡添加你的任務代碼
return StatusOK_STATUS;
}
public boolean belongsTo(Object family){
return MY_JOB_FAMILYequals(family);
}
};
我們可以使用JobManager的一系列方法針對Job Family進行操作
JobgetJobManager()cancel(MY_JOB_FAMILY); //取消所有屬於MY_JOB_FAMILY的所有Job
JobgetJobManager()join(MY_JOB_FAMILY); //等待屬於MY_JOB_FAMILY的所有Job結束
JobgetJobManager()sleep(MY_JOB_FAMILY); //將所有屬於MY_JOB_FAMILY的Job轉入睡眠狀態
JobgetJobManager()wakeup(MY_JOB_FAMILY); //將所有屬於MY_JOB_FAMILY的Job喚醒
線程死鎖的調試和解決技巧
一旦我們使用了線程我們的程序中就有可能有死鎖的發生一旦發生死鎖我們發生死鎖的線程會沒有響應導致我們程序性能下降如果我們的UI線程發生了死鎖我們的程序會沒有響應必須要重啟程序所以在我們多線程程序開發中發現死鎖的情況解決死鎖問題對提高我們程序的穩定性和性能極為重要
如果我們發現程序運行異常(例如程序沒有響應)我們首先要確定是否發生了死鎖通過下面這些步驟我們可以確定是否死鎖以及死鎖的線程
在Eclipse中以Debug模式運行程序
執行響應的測試用例重現問題
在Eclipse的Debug View中選中主線程(Thread[main])
選擇菜單Run
>Suspend
這時Eclipse會展開主線程的函數調用棧
我們就可以看到當前主線程正在執行的操作
通常
Eclipse在等待用戶的操作
它的函數調用棧會和以下類似
圖片示例
如果主線程發生死鎖
函數調用棧的最上層一般會是你自己的函數調用
你可以查看一下你當前的函數調用以確定主線程在等待什麼
使用同樣的方法查看其他線程
特別是那些等待UI線程的線程
我們需要找出當前線程相互的等待關系以便找出死鎖的原因我們找出死鎖的線程後就可以針對不同情況進行處理
減小鎖的粒度
增加並發性
調整資源請求的次序
將需要等待資源的任務放到獨立的線程中執行
Job使用中要注意的問題
不要在Job中使用Thread
sleep方法
如果你想要讓Job進入睡眠狀態
最好用Job的sleep方法
雖然
使用Thread
sleep和Job的sleep方法達到的效果差不多
但是它們實現的方式完全不同
對系統的影響也不一樣
我們知道Eclipse中Job是由Eclipse的JobManager來管理的
如果我們調用Job的sleep方法
JobManager會將Job轉入睡眠狀態
與其對應的線程也會重新放入線程池等待運行其他Job
而如果我們在Job中直接調用Thread
sleep方法
它會直接使運行Job的線程進入睡眠狀態
其他Job就不可能重用這個線程了
同時
雖然運行該Job的線程進入了睡眠狀態
Job的狀態還是Running(運行狀態)
我們也不能用Job的wakeup方法喚醒該Job了
Job的取消
一般我們會直觀的認為
一旦調用Job的cancel方法
Job就會停止運行
實際上
這並不一定正確
當Job處於不同的狀態時
我們調用Job的cancel方法所起的效果是不同的
當Job在WAITING狀態和SLEEPING狀態時
一旦我們調用cancel方法
JobManager會將Job直接從等待運行的隊列中刪除
Job不會再運行了
這時cancel方法會返回true
但是如果Job正在運行
cancel方法調用並不會立即終止Job的運行
它只會設定一個標志
指明這個Job已經被取消了
我們可以使用Job的run方法傳入的參數IProgressMonitor monitor
這個參數的isCanceled方法會返回Job是否被取消的狀態
如果需要
我們必須在我們的代碼的適當位置檢查Job是否被取消的標志
作出適當的響應
另外
由於調用Job的cancel方法不一定立即終止Job
如果我們需要等待被取消的Job運行完再執行
我們可以用如下代碼
if (!jobcancel())
jobjoin();
Join方法的使用
由於join方法會導致一個線程等待另一個線程
一旦等待線程中擁有一個被等待線程所需要的鎖
就會產生死鎖
當我們的線程中需要用到同步時
這種死鎖的情況非常容易出現
所以我們使用join時必須非常小心
盡量以其他方法替代
避免過時的Job造成的錯誤
由於我們啟動的線程並不一定是馬上執行的
當我們的Job開始運行時
情況可能發生了變化
我們在Job的處理代碼中要考慮到這些情況
一種典型的情況是
我們在啟動一個對話框或者初始化一個ViewPart時
我們會啟動一些 Job去完成一些數據讀取的工作
一旦數據讀取結束
我們會啟動新的UI Job更新相應的UI
有時
用戶在打開對話框或者View後
馬上關閉了該對話框或者View
這時我們啟動的線程並沒有被中斷
一旦在Job中再去更新UI
就會出錯
在我們的代碼中必須作相應的處理
所以
我們在線程中更新界面元素之前
我們必須先檢查相應的控件是否已經被dispose了
結束語
在我們進行基於Eclipse的客戶端開發時使用多線程可以大大的提供我們的程序並發處理能力同時對於提高用戶體驗也有很好的幫助但是多線程程序也有其不利的一面我們也不要濫用線程
首先
多線程程序會大大的提高我們程序的復雜度
使得我們的開發和調試更加困難
其次
過多的線程容易引發死鎖
數據同步等並發問題的發生
另外
由於線程創建和銷毀需要開銷
程序的整體性能可能因為過多線程的使用而下降
所以我們在使用線程時一定要謹慎本文對Eclipse線程的討論希望能對大家使用線程有所幫助由於實際情況較為復雜文中所提到的方法僅供參考讀者對於不同的實際問題需要進行具體分析從而找出最佳的解決方案
From:http://tw.wingwit.com/Article/program/Java/ky/201311/28196.html