關於解決 Java 程式語言執行緒問題的建議(2)(轉)

ba發表於2007-08-15
關於解決 Java 程式語言執行緒問題的建議(2)(轉)[@more@]在《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 是任意物件。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617731/viewspace-958394/,如需轉載,請註明出處,否則將追究法律責任。

相關文章