在語言級支持鎖定對象和線程間發信使編寫線程安全類變得簡單
本文使用簡單的編程示例來說明開發高效的線程安全類是多麼有效而直觀
Java 編程語言為編寫多線程應用程序提供強大的語言支持但是編寫有用的沒有錯誤的多線程程序仍然比較困難本文試圖概述幾種方法程序員可用這幾種方法來創建高效的線程安全類
1.並發性
只有當要解決的問題需要一定程度的並發性時程序員才會從多線程應用程序中受益例如如果打印隊列應用程序僅支持一台打印機和一台客戶機則不應該將它編寫為多線程一般說來包含並發性的編碼問題通常都包含一些可以並發執行的操作同時也包含一些不可並發執行的操作例如為多個客戶機和一個打印機提供服務的打印隊列可以支持對打印的並發請求但向打印機的輸出必須是串行形式的多線程實現還可以改善交互式應用程序的響應時間
2.Synchronized 關鍵字
雖然多線程應用程序中的大多數操作都可以並行進行但也有某些操作(如更新全局標志或處理共享文件)不能並行進行在這些情況下必須獲得一個鎖來防止其他線程在執行此操作的線程完成之前訪問同一個方法在 Java 程序中這個鎖是通過 synchronized 關鍵字提供的清單 說明了它的用法
清單 使用 synchronized 關鍵字來獲取鎖
public class MaxScore {
int max;
public MaxScore() {
max = ;
}
public synchronized void currentScore(int s) {
if(s> max) {
max = s;
}
}
public int max() {
return max;
}
}
這裡兩個線程不能同時調用 currentScore() 方法當一個線程工作時另一個線程必須阻塞但是可以有任意數量的線程同時通過 max() 方法訪問最大值因為 max() 不是同步方法因此它與鎖定無關
試考慮在 MaxScore 類中添加另一個方法的影響該方法的實現如清單 所示
清單 添加另一個方法
public synchronized void reset() {
max = ;
}
這個方法(當被訪問時)不僅將阻塞 reset() 方法的其他調用而且也將阻塞 MaxScore 類的同一個實例中的 currentScore() 方法因為這兩個方法都訪問同一個鎖如果兩個方法必須不彼此阻塞則程序員必須在更低的級別使用同步清單 是另一種情況其中兩個同步的方法可能需要彼此獨立
清單 兩個獨立的同步方法
import javautil*;
public class Jury {
Vector members;
Vector alternates;
public Jury() {
members = new Vector( );
alternates = new Vector( );
}
public synchronized void addMember(String name) {
membersadd(name);
}
public synchronized void addAlt(String name) {
alternatesadd(name);
}
public synchronized Vector all() {
Vector retval = new Vector(members);
retvaladdAll(alternates);
return retval;
}
}
此處兩個不同的線程可以將 members 和 alternates 添加到 Jury 對象中請記住synchronized 關鍵字既可用於方法更一般地也可用於任何代碼塊清單 中的兩段代碼是等效的
清單 等效的代碼
synchronized void f() { void f() {
// 執行某些操作
synchronized(this) {
} // 執行某些操作
}
}
所以為了確保 addMember() 和 addAlt() 方法不彼此阻塞可按清單 所示重寫 Jury 類
清單 重寫後的 Jury 類
import javautil*;
public class Jury {
Vector members;
Vector alternates;
public Jury() {
members = new Vector( );
alternates = new Vector( );
}
public void addMember(String name) {
synchronized(members) {
membersadd(name);
}
}
public void addAlt(String name) {
synchronized(alternates) {
alternatesadd(name);
}
}
public Vector all() {
Vector retval;
synchronized(members) {
retval = new Vector(members);
}
synchronized(alternates) {
retvaladdAll(alternates);
}
return retval;
}
}
請注意我們還必須修改 all() 方法因為對 Jury 對象同步已沒有意義在改寫後的版本中addMember()addAlt() 和 all() 方法只訪問與 members 和 alternates 對象相關的鎖因此鎖定 Jury 對象毫無用處另請注意all() 方法本來可以寫為清單 所示的形式
清單 將 members 和 alternates 用作同步的對象
public Vector all() {
synchronized(members) {
synchronized(alternates) {
Vector retval;
retval = new Vector(members);
retvaladdAll(alternates);
}
}
return retval;
}
但是因為我們早在需要之前就獲得 members 和 alternates 的鎖所以這效率不高清單 中的改寫形式是一個較好的示例因為它只在最短的時間內持有鎖並且每次只獲得一個鎖這樣就完全避免了當以後增加代碼時可能產生的潛在死鎖問題
3.同步方法的分解
正如在前面看到的那樣同步方法獲取對象的一個鎖如果該方法由不同的線程頻繁調用則此方法將成為瓶頸因為它會對並行性造成限制從而會對效率造成限制這樣作為一個一般的原則應該盡可能地少用同步方法盡管有這個原則但有時一個方法可能需要完成需要鎖定一個對象幾項任務同時還要完成相當耗時的其他任務在這些情況下可使用一個動態的鎖定釋放鎖定釋放方法例如清單 和清單 顯示了可按這種方式變換的代碼
清單 最初的低效率代碼
public synchonized void doWork() {
unsafe();
write_file();
unsafe();
}
清單 重寫後效率較高的代碼
public void doWork() {
synchonized(this) {
unsafe();
}
write_file();
synchonized(this) {
unsafe();
}
}
清單 和清單 假定第一個和第三個方法需要對象被鎖定而更耗時的 write_file() 方法不需要對象被鎖定如您所見重寫此方法以後對此對象的鎖在第一個方法完成以後被釋放然後在第三個方法需要時重新獲得這樣當 write_file() 方法執行時等待此對象的鎖的任何其他方法仍然可以運行將同步方法分解為這種混合代碼可以明顯改善性能但是您需要注意不要在這種代碼中引入邏輯錯誤
4.嵌套類
內部類在 Java 程序中實現了一個令人關注的概念它允許將整個類嵌套在另一個類中嵌套類作為包含它的類的一個成員變量如果定期被調用的的一個特定方法需要一個類就可以構造一個嵌套類此嵌套類的唯一任務就是定期調用所需的方法這消除了對程序的其他部分的相依性並使代碼進一步模塊化清單 一個圖形時鐘的基礎使用了內部類
清單 圖形時鐘示例
public class Clock {
protected class Refresher extends Thread {
int refreshTime;
public Refresher(int x) {
super(Refresher);
refreshTime = x;
}
public void run() {
while(true) {
try {
sleep(refreshTime);
}
catch(Exception e) {}
repaint();
}
}
}
public Clock() {
Refresher r = new Refresher();
rstart();
}
private void repaint() {
// 獲取時間的系統調用
// 重繪時鐘指針
}
}
清單 中的代碼示例不靠任何其他代碼來調用 repaint() 方法這樣將一個時鐘並入一個較大的用戶界面就相當簡單
5.事件驅動處理
當應用程序需要對事件或條件(內部的和外部的)作出反映時有兩種方法或用來設計系統
在第一種方法(稱為輪詢)中系統定期確定這一狀態並據此作出反映這種方法(雖然簡單)也效率不高因為您始終無法預知何時需要調用它
第二種方法(稱為事件驅動處理)效率較高但實現起來也較為復雜在事件驅動處理的情況下需要一種發信機制來控制某一特定線程何時應該運行在 Java 程序中您可以使用 wait()notify() 和 notifyAll() 方法向線程發送信號這些方法允許線程在一個對象上阻塞直到所需的條件得到滿足為止然後再次開始運行這種設計減少了 CPU 占用因為線程在阻塞時不消耗執行時間並且可在 notify() 方法被調用時立即喚醒與輪詢相比事件驅動方法可以提供更短的響應時間
6.創建高效的線程安全類的步驟
編寫線程安全類的最簡單的方法是用 synchronized 聲明每個方法雖然這種方案可以消除數據損壞但它同時也會消除您預期從多線程獲得的任何收益這樣您就需要分析並確保在 synchronized 塊內部僅占用最少的執行時間您必須格外關注訪問緩慢資源(文件目錄網絡套接字和數據庫)的方法這些方法可能降低您的程序的效率盡量將對這類資源的訪問放在一個單獨的線程中最好在任何 synchronized 代碼之外
一個線程安全類的示例被設計為要處理的文件的中心儲存庫它與使用 getWork() 和 finishWork() 與 WorkTable 類對接的一組線程一起工作本例旨在讓您體驗一下全功能的線程安全類該類使用了 helper 線程和混合同步請注意繼續添加要處理的新文件的Refresher helper 線程的用法本例沒有調整到最佳性能很明顯有許多地方可以改寫以改善性能比如將 Refresher 線程改為使用 wait()/notify() 方法事件驅動的改寫 populateTable() 方法以減少列出磁盤上的文件(這是高成本的操作)所產生的影響
7.小結
通過使用可用的全部語言支持Java 程序中的多線程編程相當簡單但是使線程安全類具有較高的效率仍然比較困難為了改善性能您必須事先考慮並謹慎使用鎖定功能
From:http://tw.wingwit.com/Article/program/Java/gj/201311/27666.html