深刻淺出 Java Concurrency (37): 併發總結 part 1 死鎖與活躍度[轉]

死鎖與活躍度

前面談了不少併發的特性和工具,可是大部分都是和鎖有關的。咱們使用鎖來保證線程安全,可是這也會引發一些問題。安全

 
  • 鎖順序死鎖(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);
        }
    }
}


當咱們銀行轉帳的時候,咱們指望鎖住雙方的帳戶,這樣保證是原子操做。 看起來很合理,但是若是雙方同時在進行轉帳操做,那麼就有可能發生死鎖的可能性。spa

 

很顯然,動態的鎖順序死鎖的解決方案應該看起來和鎖順序死鎖解決方案差很少。 可是一個比較特殊的解決方式是糾正這種順序。 例如能夠調整成這樣:線程

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)。 嚴格意義上講,這種死鎖更像是活躍度問題。例如提交到線程池中的任務因爲老是不可以搶到線程從而一直不被執行, 形成任務的「假死」情況。blog

除了上述幾種問題外,還有協做對象間的死鎖以及開發調用的問題。這個描述起來會比較困難,也不容易看出死鎖來。隊列

 

避免和解決死鎖

一般發生死鎖後程序難以自恢復。但也不是不能避免的。 有一些技巧和原則是能夠下降死鎖可能性的。

最簡單的原則是儘量的減小鎖的範圍。鎖的範圍越小,那麼競爭的可能性也越小。 儘快釋放鎖也有助於避開鎖順序。若是一個線程每次最多隻可以獲取一個鎖,那麼就不會產生鎖順序死鎖。儘管應用中比較困難,可是減小鎖的邊界有助於分析程序的設計和簡化流程。 減小鎖之間的依賴以及遵照獲取鎖的順序是避免鎖順序死鎖的有效途徑。

另外儘量的使用定時的鎖有助於程序從死鎖中自恢復。 例如對於上述順序鎖死鎖中,使用定時鎖很容易解決此問題。

 

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)是指線程雖然沒有被阻塞,可是因爲某種條件不知足,一直嘗試重試,卻終是失敗。

考慮一個場景,咱們從隊列中拿出一個任務來執行,若是任務執行失敗,那麼將任務從新加入隊列,繼續執行。假如任務老是執行失敗,或者某種依賴的條件老是不知足,那麼線程一直在繁忙卻沒有任何結果。

錯誤的循環引用和判斷也有可能致使活鎖。當某些條件老是不能知足的時候,可能陷入死循環的境地。

線程間的協同也有可能致使活鎖。例如若是兩個線程發生了某些條件的碰撞後從新執行,那麼若是再次嘗試後依然發生了碰撞,長此下去就有可能發生活鎖。

解決活鎖的一種方案是對重試機制引入一些隨機性。例如若是檢測到衝突,那麼就暫停隨機的必定時間進行重試。這回大大減小碰撞的可能性。

另外爲了不可能的死鎖,適當加入必定的重試次數也是有效的解決辦法。儘管這在業務上會引發一些複雜的邏輯處理。

相關文章
相關標籤/搜索