歡迎您光臨本站 註冊首頁

關於解決 Java 編程語言線程問題的建議(4)

←手機掃碼閱讀     火星人 @ 2014-03-26 , reply:0

訪問的問題

如果缺少良好的訪問控制,會使線程編程非常困難。大多數情況下,如果能保證線程只從同步子系統中調用,不必考慮線程安全(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 的提議並不如本文所推薦的那麼廣,他主要致力於讓現有的線程模型以更為合理方式運行。更多信息可從 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。

[火星人 ] 關於解決 Java 編程語言線程問題的建議(4)已經有476次圍觀

http://coctec.com/docs/linux/show-post-190066.html