關于解決 Java 編程語言線程問題的建議(2)
發表于:2007-07-14來源:作者:點擊數:
標簽:
在《Taming Java Threads》的第八章中,我給出了一個 服務器 端的 socket 處理程序,作為線程池的例子。它是關于使用線程池的任務的一個好例子。其基本思路是產生一個獨立對象,它的任務是監控一個服務器端的 socket。每當一個客戶機連接到服務器時,服務器
在《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.a
clearcase/" target="_blank" >ccept() );
}
}
}
//...
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 派生類,它在等待超時后即被拋出。
超時是需要的,但還不足以使代碼強壯。您還需要具備從外部中止請求鎖等待的能力。所以,當向一個等待鎖的線程傳送一個
inter
rupt() 方法后,此方法應拋出一個 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 是任意對象。
原文轉自:http://www.anti-gravitydesign.com