程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 關於解決Java編程語言線程問題的建議

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

編輯:關於JAVA

Allen Holub 指出,Java 編程語言的線程模型可能是此語言中最薄弱的部分。它完全不適合實際復雜程序的要求,而且也完全不是面向對象的。本文建議對 Java 語言進行重大修改和補充,以解決這些問題。

Java 語言的線程模型是此語言的一個最難另人滿意的部分。盡管 Java 語言本身就支持線程編程是件好事,但是它對線程的語法和類包的支持太少,只能適用於極小型的應用環境。

關於 Java 線程編程的大多數書籍都長篇累牍地指出了 Java 線程模型的缺陷,並提供了解決這些問題的急救包(Band-Aid/邦迪創可貼)類庫。我稱這些類為急救包,是因為它們所能解決的問題本應是由 Java 語言本身語法所包含的。從長遠來看,以語法而不是類庫方法,將能產生更高效的代碼。這是因為編譯器和 Java 虛擬器 (JVM) 能一同優化程序代碼,而這些優化對於類庫中的代碼是很難或無法實現的。

在我的《 Taming Java Threads》(請參閱 參考資料 )書中以及本文中,我進一步建議對 Java 編程語言本身進行一些修改,以使得它能夠真正解決這些線程編程的問題。本文和我這本書的主要區別是,我在撰寫本文時進行了更多的思考, 所以對書中的提議加以了提高。這些建議只是嘗試性的 -- 只是我個人對這些問題的想法,而且實現這些想法需要進行大量的工作以及同行們的評價。但這是畢竟是一個開端,我有意為解決這些問題成立一個專門的工作組,如果您感興趣,請發 e-mail 到 [email protected]。一旦我真正著手進行,我就會給您發通知。

這裡提出的建議是非常大膽的。有些人建議對 Java 語言規范 (JLS)(請參閱 參考資料 )進行細微和少量的修改以解決當前模糊的 JVM 行為,但是我卻想對其進行更為徹底的改進。

在實際草稿中,我的許多建議包括為此語言引入新的關鍵字。雖然通常要求不要突破一個語言的現有代碼是正確的,但是如果該語言的並不是要保持不變以至於過時的話,它就必須能引入新的關鍵字。為了使引入的關鍵字與現有的標識符不產生沖突,經過細心考慮,我將使用一個 ($) 字符,而這個字符在現有的標識符中是非法的。(例如,使用 $task,而不是 task)。此時需要編譯器的命令行開關提供支持,能使用這些關鍵字的變體,而不是忽略這個美元符號。

task(任務)的概念

Java 線程模型的根本問題是它完全不是面向對象的。面向對象 (OO) 設計人員根本不按線程角度考慮問題;他們考慮的是 同步信息 異步 信息(同步信息被立即處理 -- 直到信息處理完成才返回消息句柄;異步信息收到後將在後台處理一段時間 -- 而早在信息處理結束前就返回消息句柄)。Java 編程語言中的 Toolkit.getImage() 方法就是異步信息的一個好例子。 getImage() 的消息句柄將被立即返回,而不必等到整個圖像被後台線程取回。

這是面向對象 (OO) 的處理方法。但是,如前所述,Java 的線程模型是非面向對象的。一個 Java 編程語言線程實際上只是一個 run() 過程,它調用了其它的過程。在這裡就根本沒有對象、異步或同步信息以及其它概念。

對於此問題,在我的書中深入討論過的一個解決方法是,使用一個 Active_object。 active 對象是可以接收異步請求的對象,它在接收到請求後的一段時間內以後台方式得以處理。在 Java 編程語言中,一個請求可被封裝在一個對象中。例如,你可以把一個通過 Runnable 接口實現的實例傳送給此 active 對象,該接口的 run() 方法封裝了需要完成的工作。該 runnable 對象被此 active 對象排入到隊列中,當輪到它執行時,active 對象使用一個後台線程來執行它。

在一個 active 對象上運行的異步信息實際上是同步的,因為它們被一個單一的服務線程按順序從隊列中取出並執行。因此,使用一個 active 對象以一種更為過程化的模型可以消除大多數的同步問題。

在某種意義上,Java 編程語言的整個 Swing/AWT 子系統是一個 active 對象。向一個 Swing 隊列傳送一條訊息的唯一安全的途徑是,調用一個類似 SwingUtilities.invokeLater() 的方法,這樣就在 Swing 事件隊列上發送了一個 runnable 對象,當輪到它執行時, Swing 事件處理線程將會處理它。

那麼我的第一個建議是,向 Java 編程語言中加入一個 task (任務)的概念,從而將active 對象集成到語言中。( task的概念是從 Intel 的 RMX 操作系統和 Ada 編程語言借鑒過來的。大多數實時操作系統都支持類似的概念。)

一個任務有一個內置的 active 對象分發程序,並自動管理那些處理異步信息的全部機制。

定義一個任務和定義一個類基本相同,不同的只是需要在任務的方法前加一個 asynchronous 修飾符來指示 active 對象的分配程序在後台處理這些方法。請參考我的書中第九章的基於類方法,再看以下的 file_io 類,它使用了在《 Taming Java Threads 》中所討論的 Active_object 類來實現異步寫操作:

interface Exception_handler
    {  void handle_exception( Throwable e ); }
    class File_io_task {  Active_object dispatcher = new Active_object();
    final OutputStream   file;
    final Exception_handler handler;
    File_io_task( String file_name, Exception_handler handler )
                        throws IOException
    {  file = new FileOutputStream( file_name );
      this.handler = handler;
    }
    public void write( final byte[] bytes ) {
      // The following call asks the active-object dispatcher
      // to enqueue the Runnable object on its request
      // queue. A thread associated with the active object
      // dequeues the runnable objects and executes them
      // one at a time.
      dispatcher.dispatch
      (  new Runnable()
    {  public void run() {
            try
            {  byte[] copy new byte[ bytes.length ];
              System.arrayCopy(  bytes, 0,
                        copy,  0,
                        bytes.length );
              file.write( copy );
            }
            catch( Throwable problem )
            {  handler.handle_exception( problem );
            }
          }
        }
      );
    }
  }

所有的寫請求都用一個 dispatch() 過程調用被放在 active-object 的輸入隊列中排隊。在後台處理這些異步信息時出現的任何異常 (exception) 都由 Exception_handler 對象處理,此 Exception_handler 對象被傳送到 File_io_task 的構造函數中。您要寫內容到文件時,代碼如下:

File_io_task io =  new File_io_task
             ( "foo.txt"
               new Exception_handler
     {  public void handle( Throwable e ) {  e.printStackTrace();
                 }
               }
             );
   //...
   io.write( some_bytes );

這種基於類的處理方法,其主要問題是太復雜了 -- 對於一個這樣簡單的操作,代碼太雜了。向 Java 語言引入 $task 和 $asynchronous 關鍵字後,就可以按下面這樣重寫以前的代碼:

$task File_io $error{ $.printStackTrace(); }
   {
     OutputStream file;
     File_io( String file_name ) throws IOException
     {  file = new FileOutputStream( file_name );
     }
     asynchronous public write( byte[] bytes )
     {  file.write( bytes );
     }
   }

注意,異步方法並沒有指定返回值,因為其句柄將被立即返回,而不用等到請求的操作處理完成後。所以,此時沒有合理的返回值。對於派生出的模型, $task 關鍵字和 class 一樣同效: $task 可以實現接口、繼承類和繼承的其它任務。標有 asynchronous 關鍵字的方法由 $task 在後台處理。其它的方法將同步運行,就像在類中一樣。

$task 關鍵字可以用一個可選的 $error 從句修飾 (如上所示),它表明對任何無法被異步方法本身捕捉的異常將有一個缺省的處理程序。我使用 $ 來代表被拋出的異常對象。如果沒有指定 $error 從句,就將打印出一個合理的出錯信息(很可能是堆棧跟蹤信息)。

注意,為確保線程安全,異步方法的參數必須是不變 (immutable) 的。運行時系統應通過相關語義來保證這種不變性(簡單的復制通常是不夠的)。

所有的 task 對象必須支持一些偽信息 (pseudo-message),例如:

some_task.close() 在此調用後發送的任何異步信息都產生一個 TaskClosedException 。但是,在 active 對象隊列上等候的消息仍能被提供。 some_task.join() 調用程序被阻斷,直到此任務關閉、而且所有未完成的請求都被處理完畢。

除了常用的修飾符( public 等), task 關鍵字還應接受一個 $pooled(n) 修飾符,它導致 task 使用一個線程池,而不是使用單個線程來運行異步請求。 n 指定了所需線程池的大小;必要時,此線程池可以增加,但是當不再需要線程時,它應該縮到原來的大小。偽域 (pseudo-field) $pool_size 返回在 $pooled(n) 中指定的原始 n 參數值。

在《 Taming Java Threads 》的第八章中,我給出了一個服務器端的 socket 處理程序,作為線程池的例子。它是關於使用線程池的任務的一個好例子。其基本思路是產生一個獨立對象,它的任務是監控一個服務器端的 socket。每當一個客戶機連接到服務器時,服務器端的對象會從池中抓取一個預先創建的睡眠線程,並把此線程設置為服務於客戶端連接。socket 服務器會產出一個額外的客戶服務線程,但是當連接關閉時,這些額外的線程將被刪除。實現 socket 服務器的推薦語法如下:

public $pooled(10) $task Client_handler {
     PrintWriter log = new PrintWriter( System.out );
     public asynchronous void handle( Socket connection_to_the_client ) {
       log.println("writing");
       // client-handling code goes here. Every call to
       // handle() is executed on its own thread, but 10
       // threads are pre-created for this purpose. Additional
       // threads are created on an as-needed basis, but are
       // discarded when handle() returns.
     }
   }
   $task Socket_server
   {
     ServerSocket server;
     Client_handler client_handlers = new Client_handler();
     public Socket_server( int port_number ) {  server = new ServerSocket(port_number);
     }
     public $asynchronous listen(Client_handler client) {
       // This method is executed on its own thread.
       while( true )
       {  client_handlers.handle( server.accept() );
       }
     }
   }
   //...
   Socket_server = new Socket_server( the_port_number );
   server.listen()

Socket_server 對象使用一個獨立的後台線程處理異步的 listen() 請求,它封裝 socket 的“接受”循環。當每個客戶端連接時, listen() 請求一個 Client_handler 通過調用 handle() 來處理請求。每個 handle() 請求在它們自己的線程中執行(因為這是一個 $pooled 任務)。

注意,每個傳送到 $pooled $task 的異步消息實際上都使用它們自己的線程來處理。典型情況下,由於一個 $pooled $task 用於實現一個自主操作;所以對於解決與訪問狀態變量有關的潛在的同步問題,最好的解決方法是在 $asynchronous 方法中使用 this 是指向的對象的一個獨有副本。這就是說,當向一個 $pooled $task 發送一個異步請求時,將執行一個 clone() 操作,並且此方法的 this 指針會指向此克隆對象。線程之間的通信可通過對 static 區的同步訪問實現。

改進 synchronized

雖然在多數情況下, $task 消除了同步操作的要求,但是不是所有的多線程系統都用任務來實現。所以,還需要改進現有的線程模塊。 synchronized 關鍵字有下列缺點:

無法指定一個超時值。

無法中斷一個正在等待請求鎖的線程。

無法安全地請求多個鎖 。(多個鎖只能以依次序獲得。)

解決這些問題的辦法是:擴展 synchronized 的語法,使它支持多個參數和能接受一個超時說明(在下面的括弧中指定)。下面是我希望的語法:

synchronized(x && y && z) 獲得x 、y 和z 對象的鎖。 synchronized(x || y || z) 獲得x 、y 或z 對象的鎖。 synchronized( (x && y ) || z) 對於前面代碼的一些擴展。 synchronized(...)[1000] 設置 1 秒超時以獲得一個鎖。 synchronized[1000] f(){...} 在進入f() 函數時獲得this 的鎖,但可有 1 秒超時。

TimeoutException 是 RuntimeException 派生類,它在等待超時後即被拋出。

超時是需要的,但還不足以使代碼強壯。您還需要具備從外部中止請求鎖等待的能力。所以,當向一個等待鎖的線程傳送一個 interrupt() 方法後,此方法應拋出一個 SynchronizationException 對象,並中斷等待的線程。這個異常應是 RuntimeException 的一個派生類,這樣不必特別處理它。

對 synchronized 語法這些推薦的更改方法的主要問題是,它們需要在二進制代碼級上修改。而目前這些代碼使用進入監控(enter-monitor)和退出監控(exit-monitor)指令來實現 synchronized 。而這些指令沒有參數,所以需要擴展二進制代碼的定義以支持多個鎖定請求。但是這種修改不會比在 Java 2 中修改 Java 虛擬機的更輕松,但它是向下兼容現存的 Java 代碼。

另一個可解決的問題是最常見的死鎖情況,在這種情況下,兩個線程都在等待對方完成某個操作。設想下面的一個例子(假設的):

class Broken
{  Object lock1 = new Object();
   Object lock2 = new Object();
void a()
   {  synchronized( lock1 )
     {  synchronized( lock2 )
       {  // do something
       }
     }
   }
void b()
   {  synchronized( lock2 )
     {  synchronized( lock1 )
       {  // do something
       }
     }
   }

設想一個線程調用 a() ,但在獲得  lock1 之後在獲得 lock2 之前被剝奪運行權。 第二個線程進入運行,調用 b() ,獲得了 lock2 ,但是由於第一個線程占用 lock1 ,所以它無法獲得 lock1 ,所以它隨後處於等待狀態。此時第一個線程被喚醒,它試圖獲得 lock2 ,但是由於被第二個線程占據,所以無法獲得。此時出現死鎖。下面的 synchronize-on-multiple-objects 的語法可解決這個問題:

//...
   void a()
   {  synchronized( lock1 && lock2 )
     {
     }
   }
   void b()
   {  synchronized( lock2 && lock3 )
     {
     }
   }

編譯器(或虛擬機)會重新排列請求鎖的順序,使 lock1 總是被首先獲得,這就消除了死鎖。

但是,這種方法對多線程不一定總成功,所以得提供一些方法來自動打破死鎖。一個簡單的辦法就是在等待第二個鎖時常釋放已獲得的鎖。這就是說,應采取如下的等待方式,而不是永遠等待:

while( true )
   {  try
     {  synchronized( some_lock )[10]
       {  // do the work here.
       break;
       }
     }
     catch( TimeoutException e )
     {  continue;
     }
   }

如果等待鎖的每個程序使用不同的超時值,就可打破死鎖而其中一個線程就可運行。我建議用以下的語法來取代前面的代碼:

synchronized( some_lock )[]
   {  // do the work here.
   }

synchronized 語句將永遠等待,但是它時常會放棄已獲得的鎖以打破潛在的死鎖可能。在理想情況下,每個重復等待的超時值比前一個相差一隨機值。

改進 wait() 和 notify()

wait() / notify() 系統也有一些問題:

無法檢測 wait() 是正常返回還是因超時返回。

無法使用傳統條件變量來實現處於一個“信號”(signaled)狀態。

太容易發生嵌套的監控(monitor)鎖定。

超時檢測問題可以通過重新定義 wait() 使它返回一個 boolean 變量 (而不是 void ) 來解決。一個 true 返回值指示一個正常返回,而 false 指示因超時返回。

基於狀態的條件變量的概念是很重要的。如果此變量被設置成 false 狀態,那麼等待的線程將要被阻斷,直到此變量進入 true 狀態;任何等待 true 的條件變量的等待線程會被自動釋放。 (在這種情況下, wait() 調用不會發生阻斷。)。通過如下擴展 notify() 的語法,可以支持這個功能:

notify(); 釋放所有等待的線程,而不改變其下面的條件變量的狀態。 notify(true); 把條件變量的狀態設置為 true 並釋放任何等待的進程。其後對於wait()的調用不會發生阻斷。 notify(false); 把條件變量的狀態設置為 false (其後對於wait()的調用會發生阻斷)。

嵌套監控鎖定問題非常麻煩,我並沒有簡單的解決辦法。嵌套監控鎖定是一種死鎖形式,當某個鎖的占有線程在掛起其自身之前不釋放鎖時,會發生這種嵌套監控封鎖。下面是此問題的一個例子(還是假設的),但是實際的例子是非常多的:

class Stack
{
   LinkedList list = new LinkedList();
   public synchronized void push(Object x)
   {  synchronized(list)
     {  list.addLast( x );
       notify();
     }
   }
   public synchronized Object pop()
   {  synchronized(list)
     {  if( list.size() <= 0 )
         wait();
       return list.removeLast();
     }
   }
}

此例中,在 get() 和 put() 操作中涉及兩個鎖:一個在 Stack 對象上,另一個在 LinkedList 對象上。下面我們考慮當一個線程試圖調用一個空棧的 pop() 操作時的情況。此線程獲得這兩個鎖,然後調用 wait() 釋放 Stack 對象上 的鎖,但是沒有釋放在 list 上的鎖。如果此時第二個線程試圖向堆棧中壓入一個對象,它會在 synchronized(list) 語句上永遠掛起,而且永遠不會被允許壓入一個對象。由於第一個線程等待的是一個非空棧,這樣就會發生死鎖。這就是說,第一個線程永遠無法從 wait() 返回,因為由於它占據著鎖,而導致第二個線程永遠無法運行到 notify() 語句。

在這個例子中,有很多明顯的辦法來解決問題:例如,對任何的方法都使用同步。但是在真實世界中,解決方法通常不是這麼簡單。

一個可行的方法是,在 wait() 中按照反順序釋放當前線程獲取的 所有 鎖,然後當等待條件滿足後,重新按原始獲取順序取得它們。但是,我能想象出利用這種方式的代碼對於人們來說簡直無法理解,所以我認為它不是一個真正可行的方法。如果您有好的方法,請給我發 e-mail。

我也希望能等到下述復雜條件被實現的一天。例如:

(a && (b || c)).wait();

其中 a 、 b 和 c 是任意對象。

修改 Thread 類

同時支持搶占式和協作式線程的能力在某些服務器應用程序中是基本要求,尤其是在想使系統達到最高性能的情況下。我認為 Java 編程語言在簡化線程模型上走得太遠了,並且 Java 編程語言應支持 Posix/Solaris 的“綠色(green)線程”和“輕便(lightweight)進程”概念(在“( Taming Java Threads ”第一章中討論)。 這就是說,有些 Java 虛擬機的實現(例如在 NT 上的 Java 虛擬機)應在其內部仿真協作式進程,其它 Java 虛擬機應仿真搶占式線程。而且向 Java 虛擬機加入這些擴展是很容易的。

一個 Java 的 Thread 應始終是搶占式的。這就是說,一個 Java 編程語言的線程應像 Solaris 的輕便進程一樣工作。 Runnable 接口可以用於定義一個 Solaris 式的“綠色線程”,此線程必需能把控制權轉給運行在相同輕便進程中的其它綠色線程。

例如,目前的語法:

class My_thread implements Runnable
   {  public void run(){ /*...*/ }
   }
new Thread( new My_thread );

能有效地為 Runnable 對象產生一個綠色線程,並把它綁定到由 Thread 對象代表的輕便進程中。這種實現對於現有代碼是透明的,因為它的有效性和現有的完全一樣。

把 Runnable 對象想成為綠色線程,使用這種方法,只需向 Thread 的構造函數傳遞幾個 Runnable 對象,就可以擴展 Java 編程語言的現有語法,以支持在一個單一輕便線程有多個綠色線程。(綠色線程之間可以相互協作,但是它們可被運行在其它輕便進程 ( Thread 對象) 上的綠色進程( Runnable 對象) 搶占。)。例如,下面的代碼會為每個 runnable 對象創建一個綠色線程,這些綠色線程會共享由 Thread 對象代表的輕便進程。

new Thread( new My_runnable_object(), new My_other_runnable_object() );

現有的覆蓋(override) Thread 對象並實現 run() 的習慣繼續有效,但是它應映射到一個被綁定到一輕便進程的綠色線程。(在 Thread() 類中的缺省 run() 方法會在內部有效地創建第二個 Runnable 對象。)

線程間的協作

應在語言中加入更多的功能以支持線程間的相互通信。目前, PipedInputStream 和 PipedOutputStream 類可用於這個目的。但是對於大多數應用程序,它們太弱了。我建議向 Thread 類加入下列函數:

增加一個 wait_for_start() 方法,它通常處於阻塞狀態,直到一個線程的 run() 方法啟動。(如果等待的線程在調用 run 之前被釋放,這沒有什麼問題)。用這種方法,一個線程可以創建一個或多個輔助線程,並保證在創建線程繼續執行操作之前,這些輔助線程會處於運行狀態。

(向 Object 類)增加 $send (Object o) 和 Object=$receive() 方法,它們將使用一個內部阻斷隊列在線程之間傳送對象。阻斷隊列應作為第一個 $send() 調用的副產品被自動創建。 $send() 調用會把對象加入隊列。 $receive() 調用通常處於阻塞狀態,直到有一個對象被加入隊列,然後它返回此對象。這種方法中的變量應支持設定入隊和出隊的操作超時能力: $send (Object o, long timeout) 和 $receive (long timeout)。

對於讀寫鎖的內部支持

讀寫鎖的概念應內置到 Java 編程語言中。讀寫器鎖在“ Taming Java Threads ”(和其它地方)中有詳細討論,概括地說:一個讀寫鎖支持多個線程同時訪問一個對象,但是在同一時刻只有一個線程可以修改此對象,並且在訪問進行時不能修改。讀寫鎖的語法可以借用 synchronized 關鍵字:

static Object global_resource;
//...
public void a()
   {
     $reading( global_resource )
     {  // While in this block, other threads requesting read
       // access to global_resource will get it, but threads
       // requesting write access will block.
     }
   }
public void b()
   {
     $writing( global_resource )
     {  // Blocks until all ongoing read or write operations on
       // global_resource are complete. No read or write
       // operation or global_resource can be initiated while
       // within this block.
     }
   }
public $reading void c()
   {  // just like $reading(this)...
   }
public $writing void d()
   {  // just like $writing(this)...
   }

對於一個對象,應該只有在 $writing 塊中沒有線程時,才支持多個線程進入 $reading 塊。在進行讀操作時,一個試圖進入 $writing 塊的線程會被阻斷,直到讀線程退出 $reading 塊。 當有其它線程處於 $writing 塊時,試圖進入 $reading 或 $writing 塊的線程會被阻斷,直到此寫線程退出 $writing 塊。

如果讀和寫線程都在等待,缺省情況下,讀線程會首先進行。但是,可以使用 $writer_priority 屬性修改類的定義來改變這種缺省方式。如:

$write_priority class IO
{
   $writing write( byte[] data )
   {  //...
   }
$reading byte[] read( )
   {  //...
   }
}

訪問部分創建的對象應是非法的

當前情況下,JLS 允許訪問部分創建的對象。例如,在一個構造函數中創建的線程可以訪問正被創建的對象,既使此對象沒有完全被創建。下面代碼的結果無法確定:

class Broken
   {  private long x;
Broken()
     {  new Thread()
       {  public void run()
         {  x = -1;
         }
       }.start();
x = 0;
     }
   }

設置 x 為 -1 的線程可以和設置 x 為 0 的線程同時進行。所以,此時 x 的值無法預測。

對此問題的一個解決方法是,在構造函數沒有返回之前,對於在此構造函數中創建的線程,既使它的優先級比調用 new 的線程高,也要禁止運行它的 run() 方法。

這就是說,在構造函數返回之前, start() 請求必須被推遲。

另外,Java 編程語言應可允許構造函數的同步。換句話說,下面的代碼(在當前情況下是非法的)會象預期的那樣工作:

class Illegal
   {  private long x;
synchronized Broken()
     {  new Thread()
       {  public void run()
         {
     synchronized( Illegal.this )
           { x = -1;
} }
       }.start();
x = 0;
     }
   }

我認為第一種方法比第二種更簡潔,但實現起來更為困難。

volatile 關鍵字應象預期的那樣工作

JLS 要求保留對於 volatile 操作的請求。大多數 Java 虛擬機都簡單地忽略了這部分內容,這是不應該的。在多處理器的情況下,許多主機都出現了這種問題,但是它本應由 JLS 加以解決的。如果您對這方面感興趣,馬裡蘭大學的 Bill Pugh 正在致力於這項工作(請參閱 參考資料)。

訪問的問題

如果缺少良好的訪問控制,會使線程編程非常困難。大多數情況下,如果能保證線程只從同步子系統中調用,不必考慮線程安全(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 編程語言正在演變,所以還有可提高的前景。

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。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved