隨筆 - 303  文章 - 883  trackbacks - 0
          <2007年8月>
          2930311234
          567891011
          12131415161718
          19202122232425
          2627282930311
          2345678

          歡迎光臨! 
          閑聊 QQ:1074961813

          隨筆分類(357)

          我管理的群

          公共blog

          • n維空間
          • Email : java3d@126.com 群 : 12999758

          參與管理的論壇

          好友的blog

          我的其他blog

          朋友的網站

          搜索

          •  

          最新評論

          編寫多線程的 Java 應用程序

          如何避免當前編程中最常見的問題

          developerWorks
          文檔選項
          將此頁作為電子郵件發送

          將此頁作為電子郵件發送

          未顯示需要 JavaScript 的文檔選項



          級別: 初級

          Alex Roetter (aroetter@CS.Stanford.edu), Teton Data Systems 的軟件工程師

          2001 年 2 月 01 日

          Java Thread API 允許程序員編寫具有多處理機制優點的應用程序,在后臺處理任務的同時保持用戶所需的交互感。Alex Roetter 介紹了 Java Thread API,并概述多線程可能引起的問題以及常見問題的解決方案。

          幾乎所有使用 AWT 或 Swing 編寫的畫圖程序都需要多線程。但多線程程序會造成許多困難,剛開始編程的開發者常常會發現他們被一些問題所折磨,例如不正確的程序行為或死鎖。

          在本文中,我們將探討使用多線程時遇到的問題,并提出那些常見陷阱的解決方案。

          線程是什么?

          一個程序或進程能夠包含多個線程,這些線程可以根據程序的代碼執行相應的指令。多線程看上去似乎在并行執行它們各自的工作,就像在一臺計算機上運行著多個處理機一樣。在多處理機計算機上實現多線程時,它們確實 可以并行工作。和進程不同的是,線程共享地址空間。也就是說,多個線程能夠讀寫相同的變量或數據結構。

          編寫多線程程序時,你必須注意每個線程是否干擾了其他線程的工作。可以將程序看作一個辦公室,如果不需要共享辦公室資源或與其他人交流,所有職員就會獨立并行地工作。某個職員若要和其他人交談,當且僅當該職員在“聽”且他們兩說同樣的語言。此外,只有在復印機空閑且處于可用狀態(沒有僅完成一半的復印工作,沒有紙張阻塞等問題)時,職員才能夠使用它。在這篇文章中你將看到,在 Java 程序中互相協作的線程就好像是在一個組織良好的機構中工作的職員。

          在多線程程序中,線程可以從準備就緒隊列中得到,并在可獲得的系統 CPU 上運行。操作系統可以將線程從處理器移到準備就緒隊列或阻塞隊列中,這種情況可以認為是處理器“掛起”了該線程。同樣,Java 虛擬機 (JVM) 也可以控制線程的移動――在協作或搶先模型中――從準備就緒隊列中將進程移到處理器中,于是該線程就可以開始執行它的程序代碼。

          協作式線程 模型允許線程自己決定什么時候放棄處理器來等待其他的線程。程序開發員可以精確地決定某個線程何時會被其他線程掛起,允許它們與對方有效地合作。缺點在于某些惡意或是寫得不好的線程會消耗所有可獲得的 CPU 時間,導致其他線程“饑餓”。

          搶占式線程 模型中,操作系統可以在任何時候打斷線程。通常會在它運行了一段時間(就是所謂的一個時間片)后才打斷它。這樣的結果自然是沒有線程能夠不公平地長時間霸占處理器。然而,隨時可能打斷線程就會給程序開發員帶來其他麻煩。同樣使用辦公室的例子,假設某個職員搶在另一人前使用復印機,但打印工作在未完成的時候離開了,另一人接著使用復印機時,該復印機上可能就還有先前那名職員留下來的資料。搶占式線程模型要求線程正確共享資源,協作式模型卻要求線程共享執行時間。由于 JVM 規范并沒有特別規定線程模型,Java 開發員必須編寫可在兩種模型上正確運行的程序。在了解線程以及線程間通訊的一些方面之后,我們可以看到如何為這兩種模型設計程序。





          回頁首


          線程和 Java 語言

          為了使用 Java 語言創建線程,你可以生成一個 Thread 類(或其子類)的對象,并給這個對象發送 start() 消息。(程序可以向任何一個派生自 Runnable 接口的類對象發送 start() 消息。)每個線程動作的定義包含在該線程對象的 run() 方法中。run 方法就相當于傳統程序中的 main() 方法;線程會持續運行,直到 run() 返回為止,此時該線程便死了。





          回頁首


          上鎖

          大多數應用程序要求線程互相通信來同步它們的動作。在 Java 程序中最簡單實現同步的方法就是上鎖。為了防止同時訪問共享資源,線程在使用資源的前后可以給該資源上鎖和開鎖。假想給復印機上鎖,任一時刻只有一個職員擁有鑰匙。若沒有鑰匙就不能使用復印機。給共享變量上鎖就使得 Java 線程能夠快速方便地通信和同步。某個線程若給一個對象上了鎖,就可以知道沒有其他線程能夠訪問該對象。即使在搶占式模型中,其他線程也不能夠訪問此對象,直到上鎖的線程被喚醒、完成工作并開鎖。那些試圖訪問一個上鎖對象的線程通常會進入睡眠狀態,直到上鎖的線程開鎖。一旦鎖被打開,這些睡眠進程就會被喚醒并移到準備就緒隊列中。

          在 Java 編程中,所有的對象都有鎖。線程可以使用 synchronized 關鍵字來獲得鎖。在任一時刻對于給定的類的實例,方法或同步的代碼塊只能被一個線程執行。這是因為代碼在執行之前要求獲得對象的鎖。繼續我們關于復印機的比喻,為了避免復印沖突,我們可以簡單地對復印資源實行同步。如同下列的代碼例子,任一時刻只允許一位職員使用復印資源。通過使用方法(在 Copier 對象中)來修改復印機狀態。這個方法就是同步方法。只有一個線程能夠執行一個 Copier 對象中同步代碼,因此那些需要使用 Copier 對象的職員就必須排隊等候。

          class CopyMachine {
                                  public synchronized void makeCopies(Document d, int nCopies) {
                                  //only one thread executes this at a time
                                  }
                                  public void loadPaper() {
                                  //multiple threads could access this at once!
                                  synchronized(this) {
                                  //only one thread accesses this at a time
                                  //feel free to use shared resources, overwrite members, etc.
                                  }
                                  }
                                  }
                                  

          Fine-grain 鎖
          在對象級使用鎖通常是一種比較粗糙的方法。為什么要將整個對象都上鎖,而不允許其他線程短暫地使用對象中其他同步方法來訪問共享資源?如果一個對象擁有多個資源,就不需要只為了讓一個線程使用其中一部分資源,就將所有線程都鎖在外面。由于每個對象都有鎖,可以如下所示使用虛擬對象來上鎖:

          class FineGrainLock {
                                  MyMemberClass x, y;
                                  Object xlock = new Object(), ylock = new Object();
                                  public void foo() {
                                  synchronized(xlock) {
                                  //access x here
                                  }
                                  //do something here - but don't use shared resources
                                  synchronized(ylock) {
                                  //access y here
                                  }
                                  }
                                  public void bar() {
                                  synchronized(this) {
                                  //access both x and y here
                                  }
                                  //do something here - but don't use shared resources
                                  }
                                  }
                                  

          若為了在方法級上同步,不能將整個方法聲明為 synchronized 關鍵字。它們使用的是成員鎖,而不是 synchronized 方法能夠獲得的對象級鎖。





          回頁首


          信號量

          通常情況下,可能有多個線程需要訪問數目很少的資源。假想在服務器上運行著若干個回答客戶端請求的線程。這些線程需要連接到同一數據庫,但任一時刻只能獲得一定數目的數據庫連接。你要怎樣才能夠有效地將這些固定數目的數據庫連接分配給大量的線程?一種控制訪問一組資源的方法(除了簡單地上鎖之外),就是使用眾所周知的信號量計數 (counting semaphore)。 信號量計數將一組可獲得資源的管理封裝起來。信號量是在簡單上鎖的基礎上實現的,相當于能令線程安全執行,并初始化為可用資源個數的計數器。例如我們可以將一個信號量初始化為可獲得的數據庫連接個數。一旦某個線程獲得了信號量,可獲得的數據庫連接數減一。線程消耗完資源并釋放該資源時,計數器就會加一。當信號量控制的所有資源都已被占用時,若有線程試圖訪問此信號量,則會進入阻塞狀態,直到有可用資源被釋放。

          信號量最常見的用法是解決“消費者-生產者問題”。當一個線程進行工作時,若另外一個線程訪問同一共享變量,就可能產生此問題。消費者線程只能在生產者線程完成生產后才能夠訪問數據。使用信號量來解決這個問題,就需要創建一個初始化為零的信號量,從而讓消費者線程訪問此信號量時發生阻塞。每當完成單位工作時,生產者線程就會向該信號量發信號(釋放資源)。每當消費者線程消費了單位生產結果并需要新的數據單元時,它就會試圖再次獲取信號量。因此信號量的值就總是等于生產完畢可供消費的數據單元數。這種方法比采用消費者線程不停檢查是否有可用數據單元的方法要高效得多。因為消費者線程醒來后,倘若沒有找到可用的數據單元,就會再度進入睡眠狀態,這樣的操作系統開銷是非常昂貴的。

          盡管信號量并未直接被 Java 語言所支持,卻很容易在給對象上鎖的基礎上實現。一個簡單的實現方法如下所示:

          class Semaphore {
                                  private int count;
                                  public Semaphore(int n) {
                                  this.count = n;
                                  }
                                  public synchronized void acquire() {
                                  while(count == 0) {
                                  try {
                                  wait();
                                  } catch (InterruptedException e) {
                                  //keep trying
                                  }
                                  }
                                  count--;
                                  }
                                  public synchronized void release() {
                                  count++;
                                  notify(); //alert a thread that's blocking on this semaphore
                                  }
                                  }
                                  





          回頁首


          常見的上鎖問題

          不幸的是,使用上鎖會帶來其他問題。讓我們來看一些常見問題以及相應的解決方法:

          • 死鎖。 死鎖是一個經典的多線程問題,因為不同的線程都在等待那些根本不可能被釋放的鎖,從而導致所有的工作都無法完成。假設有兩個線程,分別代表兩個饑餓的人,他們必須共享刀叉并輪流吃飯。他們都需要獲得兩個鎖:共享刀和共享叉的鎖。假如線程 "A" 獲得了刀,而線程 "B" 獲得了叉。線程 A 就會進入阻塞狀態來等待獲得叉,而線程 B 則阻塞來等待 A 所擁有的刀。這只是人為設計的例子,但盡管在運行時很難探測到,這類情況卻時常發生。雖然要探測或推敲各種情況是非常困難的,但只要按照下面幾條規則去設計系統,就能夠避免死鎖問題:
            • 讓所有的線程按照同樣的順序獲得一組鎖。這種方法消除了 X 和 Y 的擁有者分別等待對方的資源的問題。

            • 將多個鎖組成一組并放到同一個鎖下。前面死鎖的例子中,可以創建一個銀器對象的鎖。于是在獲得刀或叉之前都必須獲得這個銀器的鎖。

            • 將那些不會阻塞的可獲得資源用變量標志出來。當某個線程獲得銀器對象的鎖時,就可以通過檢查變量來判斷是否整個銀器集合中的對象鎖都可獲得。如果是,它就可以獲得相關的鎖,否則,就要釋放掉銀器這個鎖并稍后再嘗試。

            • 最重要的是,在編寫代碼前認真仔細地設計整個系統。多線程是困難的,在開始編程之前詳細設計系統能夠幫助你避免難以發現死鎖的問題。

          • Volatile 變量. volatile 關鍵字是 Java 語言為優化編譯器設計的。以下面的代碼為例:
            class VolatileTest {
                                        public void foo() {
                                        boolean flag = false;
                                        if(flag) {
                                        //this could happen
                                        }
                                        }
                                        }
                                        

            一個優化的編譯器可能會判斷出 if 部分的語句永遠不會被執行,就根本不會編譯這部分的代碼。如果這個類被多線程訪問, flag 被前面某個線程設置之后,在它被 if 語句測試之前,可以被其他線程重新設置。用 volatile 關鍵字來聲明變量,就可以告訴編譯器在編譯的時候,不需要通過預測變量值來優化這部分的代碼。

          • 無法訪問的線程 有時候雖然獲取對象鎖沒有問題,線程依然有可能進入阻塞狀態。在 Java 編程中 IO 就是這類問題最好的例子。當線程因為對象內的 IO 調用而阻塞時,此對象應當仍能被其他線程訪問。該對象通常有責任取消這個阻塞的 IO 操作。造成阻塞調用的線程常常會令同步任務失敗。如果該對象的其他方法也是同步的,當線程被阻塞時,此對象也就相當于被冷凍住了。其他的線程由于不能獲得對象的鎖,就不能給此對象發消息(例如,取消 IO 操作)。必須確保不在同步代碼中包含那些阻塞調用,或確認在一個用同步阻塞代碼的對象中存在非同步方法。盡管這種方法需要花費一些注意力來保證結果代碼安全運行,但它允許在擁有對象的線程發生阻塞后,該對象仍能夠響應其他線程。




          回頁首


          為不同的線程模型進行設計

          判斷是搶占式還是協作式的線程模型,取決于虛擬機的實現者,并根據各種實現而不同。因此,Java 開發員必須編寫那些能夠在兩種模型上工作的程序。

          正如前面所提到的,在搶占式模型中線程可以在代碼的任何一個部分的中間被打斷,除非那是一個原子操作代碼塊。原子操作代碼塊中的代碼段一旦開始執行,就要在該線程被換出處理器之前執行完畢。在 Java 編程中,分配一個小于 32 位的變量空間是一種原子操作,而此外象 doublelong 這兩個 64 位數據類型的分配就不是原子的。使用鎖來正確同步共享資源的訪問,就足以保證一個多線程程序在搶占式模型下正確工作。

          而在協作式模型中,是否能保證線程正常放棄處理器,不掠奪其他線程的執行時間,則完全取決于程序員。調用 yield() 方法能夠將當前的線程從處理器中移出到準備就緒隊列中。另一個方法則是調用 sleep() 方法,使線程放棄處理器,并且在 sleep 方法中指定的時間間隔內睡眠。

          正如你所想的那樣,將這些方法隨意放在代碼的某個地方,并不能夠保證正常工作。如果線程正擁有一個鎖(因為它在一個同步方法或代碼塊中),則當它調用 yield() 時不能夠釋放這個鎖。這就意味著即使這個線程已經被掛起,等待這個鎖釋放的其他線程依然不能繼續運行。為了緩解這個問題,最好不在同步方法中調用 yield 方法。將那些需要同步的代碼包在一個同步塊中,里面不含有非同步的方法,并且在這些同步代碼塊之外才調用 yield

          另外一個解決方法則是調用 wait() 方法,使處理器放棄它當前擁有的對象的鎖。如果對象在方法級別上使同步的,這種方法能夠很好的工作。因為它僅僅使用了一個鎖。如果它使用 fine-grained 鎖,則 wait() 將無法放棄這些鎖。此外,一個因為調用 wait() 方法而阻塞的線程,只有當其他線程調用 notifyAll() 時才會被喚醒。





          回頁首


          線程和 AWT/Swing

          在那些使用 Swing 和/或 AWT 包創建 GUI (用戶圖形界面)的 Java 程序中,AWT 事件句柄在它自己的線程中運行。開發員必須注意避免將這些 GUI 線程與較耗時間的計算工作綁在一起,因為這些線程必須負責處理用戶時間并重繪用戶圖形界面。換句話來說,一旦 GUI 線程處于繁忙,整個程序看起來就象無響應狀態。Swing 線程通過調用合適方法,通知那些 Swing callback (例如 Mouse Listener 和 Action Listener )。 這種方法意味著 listener 無論要做多少事情,都應當利用 listener callback 方法產生其他線程來完成此項工作。目的便在于讓 listener callback 更快速返回,從而允許 Swing 線程響應其他事件。

          如果一個 Swing 線程不能夠同步運行、響應事件并重繪輸出,那怎么能夠讓其他的線程安全地修改 Swing 的狀態?正如上面提到的,Swing callback 在 Swing 線程中運行。因此他們能修改 Swing 數據并繪到屏幕上。

          但是如果不是 Swing callback 產生的變化該怎么辦呢?使用一個非 Swing 線程來修改 Swing 數據是不安全的。Swing 提供了兩個方法來解決這個問題: invokeLater()invokeAndWait() 。為了修改 Swing 狀態,只要簡單地調用其中一個方法,讓 Runnable 的對象來做這些工作。因為 Runnable 對象通常就是它們自身的線程,你可能會認為這些對象會作為線程來執行。但那樣做其實也是不安全的。事實上,Swing 會將這些對象放到隊列中,并在將來某個時刻執行它的 run 方法。這樣才能夠安全修改 Swing 狀態。





          回頁首


          總結

          Java 語言的設計,使得多線程對幾乎所有的 Applet 都是必要的。特別是,IO 和 GUI 編程都需要多線程來為用戶提供完美的體驗。如果依照本文所提到的若干基本規則,并在開始編程前仔細設計系統――包括它對共享資源的訪問等,你就可以避免許多常見和難以發覺的線程陷阱。



          參考資料



          關于作者

           

          Alex Roetter 已經有數年關于用 Java 以及其他編程語言編寫多線程應用程序的經驗,在斯坦福大學獲得了計算機科學學士學位。你可以通過 aroetter@CS.Stanford.edu 與 Alex 聯系。

          如果我是國王:關于解決 Java編程語言線程問題的建議

          developerWorks
          文檔選項
          將此頁作為電子郵件發送

          將此頁作為電子郵件發送

          未顯示需要 JavaScript 的文檔選項



          級別: 初級

          Allen Holub自由撰稿人

          2000 年 10 月 01 日

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

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

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

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

          這里提出的建議是非常大膽的。有些人建議對 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) 獲得 xyz 對象的鎖。
          synchronized(x || y || z) 獲得 xyz 對象的鎖。
          synchronized( (x && y ) || z) 對于前面代碼的一些擴展。
          synchronized(...)[1000] 設置 1 秒超時以獲得一個鎖。
          synchronized[1000] f(){...} 在進入 f() 函數時獲得 this 的鎖,但可有 1 秒超時。

          TimeoutExceptionRuntimeException 派生類,它在等待超時后即被拋出。

          超時是需要的,但還不足以使代碼強壯。您還需要具備從外部中止請求鎖等待的能力。所以,當向一個等待鎖的線程傳送一個 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();
                                  

          其中 abc 是任意對象。





          回頁首


          修改 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 對象。)





          回頁首


          線程間的協作

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

          1. 增加一個 wait_for_start() 方法,它通常處于阻塞狀態,直到一個線程的 run() 方法啟動。(如果等待的線程在調用 run 之前被釋放,這沒有什么問題)。用這種方法,一個線程可以創建一個或多個輔助線程,并保證在創建線程繼續執行操作之前,這些輔助線程會處于運行狀態。
          2. (向 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 編程語言的訪問權限概念做如下限制;

          1. 應精確使用 package 關鍵字來限制包訪問權。我認為當缺省行為的存在是任何一種計算機語言的一個瑕疵,我對現在存在這種缺省權限感到很迷惑(而且這種缺省是“包(package)”級別的而不是“私有(private)”)。在其它方面,Java 編程語言都不提供等同的缺省關鍵字。雖然使用顯式的 package 的限定詞會破壞現有代碼,但是它將使代碼的可讀性更強,并能消除整個類的潛在錯誤 (例如,如果訪問權是由于錯誤被忽略,而不是被故意忽略)。
          2. 重新引入 private protected ,它的功能應和現在的 protected 一樣,但是不應允許包級別的訪問。
          3. 允許 private private 語法指定“實現的訪問”對于所有外部對象是私有的,甚至是當前對象是的同一個類的。對于“.”左邊的唯一引用(隱式或顯式)應是 this
          4. 擴展 public 的語法,以授權它可制定特定類的訪問。例如,下面的代碼應允許 Fred 類的對象可調用 some_method() ,但是對其它類的對象,這個方法應是私有的。
                public(Fred) void some_method()
                                        {
                                        }
                                        

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

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

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

          7. 增加 $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 虛擬機在下列情況下不關閉應用程序:

          1. 有任何非后臺線程正在運行,或者:
          2. 有任何后臺線程正在執行一個 synchronized 方法或 synchronized 代碼塊。

          后臺線程在它執行完 synchronized 塊或 synchronized 方法后可被立即關閉。





          回頁首


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

          由于實用原因這也許不可行,但是我希望不要廢除 stop() (在 ThreadThreadGroup 中)。但是,我會改變 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



          參考資料

          • 您可以參閱本文在 developerWorks 全球站點上的 英文原文.

          • 本文是對 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 ThreadsTaming 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 問題。


          地震讓大伙知道:居安思危,才是生存之道。
          posted on 2007-08-03 14:23 小尋 閱讀(983) 評論(0)  編輯  收藏 所屬分類: j2se/j2ee/j2me
          主站蜘蛛池模板: 商丘市| 柘城县| 满洲里市| 定西市| 青铜峡市| 神池县| 亚东县| 保靖县| 武清区| 东阿县| 略阳县| 吴堡县| 辽源市| 西乌珠穆沁旗| 凤凰县| 启东市| 芜湖市| 米脂县| 长宁县| 资中县| 南康市| 临桂县| 祁连县| 柳江县| 衡山县| 寿光市| 景东| 永胜县| 新巴尔虎左旗| 安龙县| 黎城县| 石泉县| 辉县市| 吴旗县| 缙云县| 赤壁市| 张家界市| 永德县| 大兴区| 木兰县| 乃东县|