编写多线程Java应用程序常见问题
生活随笔
收集整理的這篇文章主要介紹了
编写多线程Java应用程序常见问题
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
幾乎所有使用?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?位的變量空間是一種原子操作,而此外象?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?程序中互相協作的線程就好像是在一個組織良好的機構中工作的職員。?
在多線程程序中,線程可以從準備就緒隊列中得到,并在可獲得的系統?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?位的變量空間是一種原子操作,而此外象?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应用程序常见问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: GUI(三)一个菜单的程序
- 下一篇: ASP防SQL注入攻击程序