編寫多線程的Java應用程序-如何避免當前編程中最常見的問題(2)

發表于:2007-07-14來源:作者:點擊數: 標簽:
信號量 通常情況下,可能有多個線程需要訪問數目很少的資源。假想在 服務器 上運行著若干個回答客戶端請求的線程。這些線程需要連接到同一 數據庫 ,但任一時刻只能獲得一定數目的數據庫連接。你要怎樣才能夠有效地將這些固定數目的數據庫連接分配給大量的線
信號量
通常情況下,可能有多個線程需要訪問數目很少的資源。假想在服務器上運行著若干個回答客戶端請求的線程。這些線程需要連接到同一數據庫,但任一時刻只能獲得一定數目的數據庫連接。你要怎樣才能夠有效地將這些固定數目的數據庫連接分配給大量的線程?一種控制訪問一組資源的方法(除了簡單地上鎖之外),就是使用眾所周知的信號量計數 (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 位的變量空間是一種原子操作,而此外象 double 和 long 這兩個 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 編程都需要多線程來為用戶提供完美的體驗。如果依照本文所提到的若干基本規則,并在開始編程前仔細設計系統??包括它對共享資源的訪問等,你就可以避免許多常見和難以發覺的線程陷阱。

資料

參考 Java 2 平臺上的 API 規范說明書(1.3 版標準):Java 2 API 文檔.
更多關于 JVM 對線程和上鎖處理的信息,可以參閱 Java 虛擬機規范說明書.
Allen Holub 的 Taming Java Threads (APress, June 2000) 是一本極好的參考書
你可能還希望閱讀 Allen 的文章 如果我是國王:關于解決 Java 編程語言線程問題的建議 (developerWorks, October 2000), 里面闡述了一些被他稱為“一門偉大語言最虛弱之處”的問題。

原文轉自:http://www.anti-gravitydesign.com

国产97人人超碰caoprom_尤物国产在线一区手机播放_精品国产一区二区三_色天使久久综合给合久久97