關于解決 Java 編程語言線程問題的建議(4)
發表于:2007-07-14來源:作者:點擊數:
標簽:
訪問的問題 如果缺少良好的訪問控制,會使線程編程非常困難。大多數情況下,如果能保證線程只從同步子系統中調用,不必考慮線程 安全 (threadsafe)問題。我建議對 Java 編程語言的訪問權限概念做如下限制; 應精確使用 package 關鍵字來限制包訪問權。我認為
訪問的問題
如果缺少良好的訪問控制,會使線程編程非常困難。大多數情況下,如果能保證線程只從同步子系統中調用,不必考慮線程
安全(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 版本中引入內部類后,編譯器中一直有這個錯誤。在此版本中(三年以后),這個錯誤依然存在?,F在,該是改正這個錯誤的時候了。
對于類級域的實例級訪問
除了訪問權限外,還有一個問題,即類級(靜態)方法和實例(非靜態)方法都能直接訪問類級(靜態)域。這種訪問是非常危險的,因為實例方法的同步不會獲取類級的鎖,所以一個
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 a
clearcase/" target="_blank" >ccessor( 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 操作系統不能很好地處理一個外部指示的突然停止(ab
rupt)。(它不把
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 的提議并不如本文所推薦的那么廣,他主要致力于讓現有的線程模型以更為合理方式運行。更多信息可從 www.cs.umd.edu/~pugh/
java/memoryModel/ 獲得。
從 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 取得聯系并獲取信息:www.holub.com。
原文轉自:http://www.anti-gravitydesign.com