死鎖與活躍度
前面談了很多并發的特性和工具,但是大部分都是和鎖有關的。我們使用鎖來保證線程安全,但是這也會引起一些問題。
- 鎖順序死鎖(lock-ordering deadlock):多個線程試圖通過不同的順序獲得多個相同的資源,則發生的循環鎖依賴現象。
- 動態的鎖順序死鎖(Dynamic Lock Order Deadlocks):多個線程通過傳遞不同的鎖造成的鎖順序死鎖問題。
- 資源死鎖(Resource Deadlocks):線程間相互等待對方持有的鎖,并且誰都不會釋放自己持有的鎖發生的死鎖。也就是說當現場持有和等待的目標成為資源,就有可能發生此死鎖。這和鎖順序死鎖不一樣的地方是,競爭的資源之間并沒有嚴格先后順序,僅僅是相互依賴而已。
鎖順序死鎖
最經典的鎖順序死鎖就是LeftRightDeadLock.
public class LeftRightDeadLock {
final Object left = new Object();
final Object right = new Object();
public void doLeftRight() {
synchronized (left) {
synchronized (right) {
execute1();
}
}
}
public void doRightLeft() {
synchronized (right) {
synchronized (left) {
execute2();
}
}
}
private void execute2() {
}
private void execute1() {
}
}
這個例子很簡單,當兩個線程分別獲取到left和right鎖時,互相等待對方釋放其對應的鎖,很顯然雙方都陷入了絕境。
動態的鎖順序死鎖
與鎖順序死鎖不同的是動態的鎖順序死鎖只是將靜態的鎖變成了動態鎖。 一個比較生動的例子是這樣的。
public void transferMoney(Account fromAccount,//
Account toAccount,//
int amount
) {
synchronized (fromAccount) {
synchronized (toAccount) {
fromAccount.decr(amount);
toAccount.add(amount);
}
}
}
當我們銀行轉賬的時候,我們期望鎖住雙方的賬戶,這樣保證是原子操作。 看起來很合理,可是如果雙方同時在進行轉賬操作,那么就有可能發生死鎖的可能性。
很顯然,動態的鎖順序死鎖的解決方案應該看起來和鎖順序死鎖解決方案差不多。 但是一個比較特殊的解決方式是糾正這種順序。 例如可以調整成這樣:
Object lock = new Object();
public void transferMoney(Account fromAccount,//
Account toAccount,//
int amount
) {
int order = fromAccount.name().compareTo(toAccount.name());
Object lockFirst = order>0?toAccount:fromAccount;
Object lockSecond = order>0?fromAccount:toAccount;
if(order==0){
synchronized(lock){
synchronized(lockFirst){
synchronized(lockSecond){
//do work
}
}
}
}else{
synchronized(lockFirst){
synchronized(lockSecond){
//do work
}
}
}
}
這個挺有意思的。比較兩個賬戶的順序,保證此兩個賬戶之間的傳遞順序總是按照某一種鎖的順序進行的, 即使多個線程同時發生,也會遵循一次操作完釋放完鎖才進行下一次操作的順序,從而可以避免死鎖的發生。
資源死鎖
資源死鎖比較容易理解,就是需要的資源遠遠大于已有的資源,這樣就有可能線程間的資源競爭從而發生死鎖。 一個簡單的場景是,應用同時從兩個連接池中獲取資源,兩個線程都在等待對方釋放連接池的資源以便能夠同時獲取 到所需要的資源,從而發生死鎖。
資源死鎖除了這種資源之間的直接依賴死鎖外,還有一種叫線程饑餓死鎖(thread-starvation deadlock)。 嚴格意義上講,這種死鎖更像是活躍度問題。例如提交到線程池中的任務由于總是不能夠搶到線程從而一直不被執行, 造成任務的“假死”狀況。
除了上述幾種問題外,還有協作對象間的死鎖以及開發調用的問題。這個描述起來會比較困難,也不容易看出死鎖來。
避免和解決死鎖
通常發生死鎖后程序難以自恢復。但也不是不能避免的。 有一些技巧和原則是可以降低死鎖可能性的。
最簡單的原則是盡可能的減少鎖的范圍。鎖的范圍越小,那么競爭的可能性也越小。 盡快釋放鎖也有助于避開鎖順序。如果一個線程每次最多只能夠獲取一個鎖,那么就不會產生鎖順序死鎖。盡管應用中比較困難,但是減少鎖的邊界有助于分析程序的設計和簡化流程。 減少鎖之間的依賴以及遵守獲取鎖的順序是避免鎖順序死鎖的有效途徑。
另外盡可能的使用定時的鎖有助于程序從死鎖中自恢復。 例如對于上述順序鎖死鎖中,使用定時鎖很容易解決此問題。
public void doLeftRight() throws Exception {
boolean over = false;
while (!over) {
if (left.tryLock(1, TimeUnit.SECONDS)) {
try {
if (right.tryLock(1, TimeUnit.SECONDS)) {
try {
execute1();
} finally {
right.unlock();
over = true;
}
}
} finally {
left.unlock();
}
}
}
}
public void doRightLeft() throws Exception {
boolean over = false;
while (!over) {
if (right.tryLock(1, TimeUnit.SECONDS)) {
try {
if (left.tryLock(1, TimeUnit.SECONDS)) {
try {
execute2();
} finally {
left.unlock();
over = true;
}
}
} finally {
right.unlock();
}
}
}
}
看起來代碼會比較復雜,但是這是避免死鎖的有效方式。
活躍度
對于多線程來說,死鎖是非常嚴重的系統問題,必須修正。除了死鎖,遇到很多的就是活躍度問題了。 活躍度問題主要包括:饑餓,丟失信號,和活鎖等。
饑餓
饑餓是指線程需要訪問的資源被永久拒絕,以至于不能在繼續進行。 比如說:某個權重比較低的線程可能一直不能夠搶到CPU周期,從而一直不能夠被執行。
也有一些場景是比較容易理解的。對于一個固定大小的連接池中,如果連接一直被用完,那么過多的任務可能由于一直無法搶占到連接從而不能夠被執行。這也是饑餓的一種表現。
對于饑餓而言,就需要平衡資源的競爭,例如線程的優先級,任務的權重,執行的周期等等。總之,當空閑的資源較多的情況下,發生饑餓的可能性就越小。
弱響應性
弱響應是指,線程最終能夠得到有效的執行,只是等待的響應時間較長。 最常見的莫過于GUI的“假死”了。很多時候GUI的響應只是為了等待后臺數據的處理,如果線程協調不好,很有可能就會發生“失去響應”的現象。
另外,和饑餓很類似的情況。如果一個線程長時間獨占一個鎖,那么其它需要此鎖的線程很有可能就會被迫等待。
活鎖
活鎖(Livelock)是指線程雖然沒有被阻塞,但是由于某種條件不滿足,一直嘗試重試,卻終是失敗。
考慮一個場景,我們從隊列中拿出一個任務來執行,如果任務執行失敗,那么將任務重新加入隊列,繼續執行。假如任務總是執行失敗,或者某種依賴的條件總是不滿足,那么線程一直在繁忙卻沒有任何結果。
錯誤的循環引用和判斷也有可能導致活鎖。當某些條件總是不能滿足的時候,可能陷入死循環的境地。
線程間的協同也有可能導致活鎖。例如如果兩個線程發生了某些條件的碰撞后重新執行,那么如果再次嘗試后依然發生了碰撞,長此下去就有可能發生活鎖。
解決活鎖的一種方案是對重試機制引入一些隨機性。例如如果檢測到沖突,那么就暫停隨機的一定時間進行重試。這回大大減少碰撞的可能性。
另外為了避免可能的死鎖,適當加入一定的重試次數也是有效的解決辦法。盡管這在業務上會引起一些復雜的邏輯處理。
©2009-2014 IMXYLZ
|求賢若渴