關於解決 Java 程式語言執行緒問題的建議(4)(轉)

ba發表於2007-08-15
關於解決 Java 程式語言執行緒問題的建議(4)(轉)[@more@]訪問的問題

如果缺少良好的訪問控制,會使執行緒程式設計非常困難。大多數情況下,如果能保證執行緒只從同步子系統中呼叫,不必考慮執行緒安全(threadsafe)問題。我建議對
Java 程式語言的訪問許可權概念做如下限制;

應精確使用 package
關鍵字來限制包訪問權。我認為當預設行為的存在是任何一種計算機語言的一個瑕疵,我對現在存在這種預設許可權感到很迷惑(而且這種預設是“包(package)”級別的而不是“私有(private)”)。
在其它方面,Java 程式語言都不提供等同的預設關鍵字。雖然使用顯式的 package
的限定詞會破壞現有程式碼,但是它將使程式碼的可讀性更強,並能消除整個類的潛在錯誤
(例如,如果訪問權是由於錯誤被忽略,而不是被故意忽略)。


重新引入 private protected,它的功能應和現在的 protected
一樣,但是不應允許包級別的訪問。


允許 private private 語法指定“實現的訪問”對於所有外部物件是私有的,甚至是當前物件是的同一個類的。對於“.”左邊的唯一引用(隱式或顯式)應是
this。


擴充套件 public 的語法,以授權它可制定特定類的訪問。例如,下面的程式碼應允許
Fred 類的物件可呼叫 some_method(),但是對其它類的物件,這個方法應是私有的。


public(Fred) void some_method()
{
}




這種建議不同於 C++ 的 "friend" 機制。 在 "friend" 機制中,它授權一個類訪問另一個類的所有私有部分。在這裡,我建議對有限的方法集合進行嚴格控制的訪問。用這種方法,一個類可以為另一個類定義一個介面,而這個介面對系統的其餘類是不可見的。一個明顯的變化是:


public(Fred, Wilma) void some_method()
{
}





除非域引用的是真正不變(immutable)的物件或 static final 基本型別,否則所有域的定義應是
private。 對於一個類中域的直接訪問違反了 OO 設計的兩個基本規則:抽象和封裝。從執行緒的觀點來看,允許直接訪問域只使對它進行非同步訪問更容易一些。



增加 $property 關鍵字。帶有此關鍵字的物件可被一個“bean
盒”應用程式訪問,這個程式使用在 Class 類中定義的反射操作(introspection) API,否則與 private private 同效。 $property
屬性可用在域和方法,這樣現有的 JavaBean getter/setter 方法可以很容易地被定義為屬性。


不變性(immutability)

由於對不變物件的訪問不需要同步,所以在多執行緒條件下,不變的概念(一個物件的值在建立後不可更改)是無價的。Java
程式設計言語中,對於不變性的實現不夠嚴格,有兩個原因:


對於一個不變物件,在其被未完全建立之前,可以對它進行訪問。這種訪問對於某些域可以產生不正確的值。


對於恆定 (類的所有域都是 final) 的定義太鬆散。對於由
final 引用指定的物件,雖然引用本身不能改變,但是物件本身可以改變狀態。

第一個問題可以解決,不允許執行緒在建構函式中開始執行
(或者在建構函式返回之前不能執行開始請求)。


對於第二個問題,透過限定 final 修飾符指向恆定物件,可以解決此問題。這就是說,對於一個物件,只有所有的域是
final,並且所有引用的物件的域也都是 final,此物件才真正是恆定的。為了不打破現有程式碼,這個定義可以使用編譯器加強,即只有一個類被顯式標為不變時,此類才是不變類。方法如下:




$immutable public class Fred
{
// all fields in this class must be final, and if the
// field is a reference, all fields in the referenced
// class must be final as well (recursively).

static int x constant = 0; // use of `final` is optional when $immutable
// is present.
}





有了 $immutable 修飾符後,在域定義中的 final 修飾符是可選的。


最後,當使用內部類(inner class)後,在 Java 編譯器中的一個錯誤使它無法可靠地建立不變物件。當一個類有重要的內部類時(我的程式碼常有),編譯器經常不正確地顯示下列錯誤資訊:

"Blank final variable ´name´ may not have been initialized.
It must be assigned a value in an initializer, or in every constructor."



既使空的 final 在每個建構函式中都有初始化,還是會出現這個錯誤資訊。自從在
1.1 版本中引入內部類後,編譯器中一直有這個錯誤。在此版本中(三年以後),這個錯誤依然存在。現在,該是改正這個錯誤的時候了。



對於類級域的例項級訪問

除了訪問許可權外,還有一個問題,即類級(靜態)方法和例項(非靜態)方法都能直接訪問類級(靜態)域。這種訪問是非常危險的,因為例項方法的同步不會獲取類級的鎖,所以一個
synchronized static 方法和一個 synchronized
方法還是能同時訪問類的域。改正此問題的一個明顯的方法是,要求在例項方法中只有使用
static 訪問方法才能訪問非不變類的 static
域。當然,這種要求需要編譯器和執行時間檢查。在這種規定下,下面的程式碼是非法的:

class Broken
{
static long x;

synchronized static void f()
{ x = 0;
}

synchronized void g()
{ x = -1;
}
};




由於 f() 和 g()
可以並行執行,所以它們能同時改變 x
的值(產生不定的結果)。請記住,這裡有兩個鎖:static
方法要求屬於 Class 物件的鎖,而非靜態方法要求屬於此類例項的鎖。當從例項方法中訪問非不變 static 域時,編譯器應要求滿足下面兩個結構中的任意一個:




class Broken
{
static long x;

synchronized private static accessor( long value )
{ x = value;
}

synchronized static void f()
{ x = 0;
}

synchronized void g()
{ accessor( -1 );
}
}





或則,編譯器應獲得讀/寫鎖的使用:



class Broken
{
static long x;

synchronized static void f()
{ $writing(x){ x = 0 };
}

synchronized void g()
{ $writing(x){ x = -1 };
}
}





另外一種方法是(這也是一種理想的方法)-- 編譯器應自動使用一個讀/寫鎖來同步訪問非不變 static 域,這樣,程式設計師就不必擔心這個問題。

後臺執行緒的突然結束

當所有的非後臺執行緒終止後,後臺執行緒都被突然結束。當後臺執行緒建立了一些全域性資源(例如一個資料庫連線或一個臨時檔案),而後臺執行緒結束時這些資源沒有被關閉或刪除就會導致問題。


對於這個問題,我建議制定規則,使 Java 虛擬機器在下列情況下不關閉應用程式:


有任何非後臺執行緒正在執行,或者:

有任何後臺執行緒正在執行一個 synchronized 方法或 synchronized 程式碼塊。


後臺執行緒在它執行完 synchronized 塊或 synchronized 方法後可被立即關閉。


重新引入 stop()、
suspend() 和 resume()
關鍵字

由於實用原因這也許不可行,但是我希望不要廢除 stop() (在 Thread 和 ThreadGroup 中)。但是,我會改變 stop()
的語義,使得呼叫它時不會破壞已有程式碼。但是,關於 stop() 的問題,請記住,當執行緒終止後,stop()
將釋放所有鎖,這樣可能潛在地使正在此物件上工作的執行緒進入一種不穩定(區域性修改)的狀態。由於停止的執行緒已釋放它在此物件上的所有鎖,所以這些物件無法再被訪問。


對於這個問題,可以重新定義 stop() 的行為,使執行緒只有在不佔有任何鎖時才立即終止。如果它佔據著鎖,我建議在此執行緒釋放最後一個鎖後才終止它。
可以使用一個和丟擲異常相似的機制來實現此行為。被停止執行緒應設定一個標誌,並且當退出所有同步塊時立即測試此標誌。如果設定了此標誌,就丟擲一個隱式的異常,
但是此異常應不再能被捕捉並且當執行緒結束時不會產生任何輸出。注意,微軟的 NT 作業系統不能很好地處理一個外部指示的突然停止(abrupt)。(它不把
stop 訊息通知動態連線庫,所以可能導致系統級的資源漏洞。)這就是我建議使用類似異常的方法簡單地導致 run() 返回的原因。


與這種和異常類似的處理方法帶來的實際問題是,你必需在每個 synchronized
塊後都插入程式碼來測試“stopped”標誌。並且這種附加的程式碼會降低系統效能並增加程式碼長度。我想到的另外一個辦法是使 stop()
實現一個“延遲的(lazy)”停止,在這種情況下,在下次呼叫 wait()
或 yield() 時才終止。我還想向 Thread
中加入一個 isStopped() 和 stopped() 方法
(此時,Thread 將像isInterrupted() 和 interrupted()
一樣工作,但是會檢測 “stop-requested”的狀態)。這種方法不向第一種那樣通用,但是可行並且不會產生過載。


應把 suspend() 和 resume() 方法放回到 Java
程式語言中,它們是很有用的,我不想被當成是幼兒園的小孩。由於它們可能產生潛在的危險(當被掛起時,一個執行緒可以佔據一個鎖)而去掉它們是沒有道理的。請讓我自己來決定是否使用它們。
如果接收的執行緒正佔據著鎖,Sun 公司應該把它們作為呼叫 suspend() 的一個執行時間異常處理(run-time exception);或者更好的方法是,延遲實際的掛起過程,直到執行緒釋放所有的鎖。


被阻斷的 I/O 應正確工作

應該能打斷任何被阻斷的操作,而不是隻讓它們 wait()
和 sleep()。我在“Taming Java Threads”的第二章中的 socket 部分討論了此問題。但是現在,對於一個被阻斷的 socket 上的
I/O 操作,打斷它的唯一辦法是關閉這個 socket,而沒有辦法打斷一個被阻斷的檔案 I/O 操作。例如,一旦開始一個讀請求並且進入阻斷狀態後,除非到它實際讀出一些東西,
否則執行緒一直出於阻斷狀態。既使關掉檔案控制程式碼也不能打斷讀操作。


還有,程式應支援 I/O 操作的超時。所有可能出現阻斷操作的物件(例如 InputStream 物件)也都應支援這種方法:

InputStream s = ...;
s.set_timeout( 1000 );





這和 Socket 類的 setSoTimeout(time)
方法是等價的。同樣地,應該支援把超時作為引數傳遞到阻斷的呼叫。


ThreadGroup 類

ThreadGroup 應該實現 Thread 中能夠改變執行緒狀態的所有方法。我特別想讓它實現 join() 方法,這樣我就可等待組中的所有執行緒的終止。


總結

以上是我的建議。就像我在標題中所說的那樣,如果我是國王...(哎)。我希望這些改變(或其它等同的方法)最終能被引入
Java 語言中。我確實認為 Java 語言是一種偉大的程式語言;但是我也認為 Java 的執行緒模型設計得還不夠完善,這是一件很可惜的事情。但是,Java
程式語言正在演變,所以還有可提高的前景。

參考資料

本文是對 Taming Java Threads 的更新摘編。該書
探討了在 Java 語言中多執行緒程式設計的陷阱和問題,並提供了一個與執行緒相關的 Java 程式包來解決這些問題。


馬里蘭大學的 Bill Pugh 正在致力修改 JLS 來提高其執行緒模型。Bill 的提議並不如本文所推薦的那麼廣,他主要致力於讓現有的執行緒模型以更為合理方式執行。更多資訊可從 獲得。


從 Sun 網站可找到全部 Java 語言的規範。


要從一個純技術角度來審視執行緒,參閱 Doug Lea 編著的 Concurrent Programming in Java: Design Principles and Patterns 第二版。這是本很棒的書,但是它的風格是非常學術化的並不一定適合所有的讀者。對《Taming Java Threads》是個很好的補充讀物。


由 Scott Oaks 和 Henry Wong 編寫的 Java Threads 比 Taming Java Threads 要輕量些,但是如果您從未編寫過執行緒程式這本書更為適合。Oaks 和 Wong 同樣實現了 Holub 提供的幫助類,而且看看對同一問題的不同解決方案總是有益的。


由 Bill Lewis 和 Daniel J. Berg 編寫的 Threads Primer: A Guide to Multithreaded Programming 是對執行緒(不限於 Java)的很好入門介紹。

Java 執行緒的一些技術資訊可在 Sun 網站上找到。

在 "Multiprocessor Safety and Java" 中 Paul Jakubik 討論了多執行緒系統的 SMP 問題。


作者簡介

Allen Holub 從 1979 年起就開始致力於計算機行業。他在各種雜誌
(Dr. Dobb´s Journal、Programmers Journal、 Byte、MSJ 和其它雜誌) 上發表了大量的文章。他為網路雜誌
JavaWorld 撰寫 “Java
工具箱”專欄,也為 IBM
developerWorks 元件技術專區 撰寫“OO-設計流程”欄目。他還領導著
ITWorld 程式設計理論和實踐討論組。


Allen 撰寫了八本書籍,最近新出的一本討論了 Java 執行緒的陷阱和缺陷《Taming Java Threads》。他長期從事設計和編制物件導向軟體。從事了
8 年的 C++ 程式設計工作後,Allen 在 1996 年由 C++ 轉向 Java。他現在視 C++ 為一個噩夢,其可怕的經歷正被逐漸淡忘。他從 1982 年起就自己和為加利弗尼亞大學伯克利分校教授計算機程式設計(首先是
C,然後是 C++ 和 MFC,現在是物件導向設計和 Java)。 Allen 也提供 Java 和麵向物件設計方面的公眾課程和私授 (in-house) 課程。他還提供物件導向設計的諮詢並承包 Java 程式設計專案。請透過此 Web 站點和 Allen 取得聯絡並獲取資訊:。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617731/viewspace-958381/,如需轉載,請註明出處,否則將追究法律責任。

相關文章