併發編程實戰學習筆記(七)——避免活躍性問題

鎖順序死鎖

定義

試圖以不同的順序去獲得相同的鎖,就可能會產生死鎖

解決辦法

如果所有線程以固定的順序來獲得鎖,那麼在程序中就不會出現鎖順序死鎖問題

動態的鎖順序死鎖

原因

鎖順序本身是動態的,無法通過相同的順序來避免死鎖問題

解決辦法

通過一致哈希算法或者其它方式來統一鎖順序,使未知順序變爲已知順序。對於極少數的哈希衝突,可以使用“加時賽”鎖來解決

private static final Object tieLock = new Object();

public void transferMoney(final Account fromAcct,
                                            final Account toAcct,
                                            final DollarAmount amount)
                           throws InsufficientFundsException{
    class Helper{
        public void transfer throws InsufficientFundsException{
              if(fromAcct.getBalance().compareTo(amount) < 0){
                  throw new InsufficientFundsException();
              }else{
                  fromAcct.debit(amount);
                  toAcct.credit(amount);
              }
          }
    }

    int fromHash = System.identifyHashCode(fromAcct);
    int toHash = System.identityHashCode(toAcct);

    if(fromHash < toHash){
        synchronized(fromAcct){
            synchronized(toAcct){
                new Helper.transfer();
            }
        }
    }else if (fromHash > toHash) {
        synchronized(toAcct){
            synchronized(fromAcct){
                new Helper().transfer();
            }
        }
    } else {
        synchronized(tieLock){//加時賽鎖來解決問題
            synchronized(fromAcct){
                synchronized(toAcct){
                    new Helper().transfer();
                }
            }
        }
    }
}

在協作對象之間發生的死鎖

如果在持有鎖時調用某個外部方法,那麼將出現活躍性問題。在這個外部方法中可能會獲取其它鎖(這可能會產生死鎖),或者阻塞時間過長,導致其它線程無法及時獲得當前被持有的鎖。
鎖順序死鎖可能以這種方式隱式出現

開放調用

如果在調用某個方法時不需要持有鎖,那麼這種調用被稱爲開放調用

簡化併發程序分析難度的可行思路

  • 雖然在沒有封裝的情況下也能確保線程安全的程序,但對一個使用了封裝的程序進行線程安全分析,要比分析沒有使用封裝的程序容易得多。
  • 分析一個完全依賴開放調用的程序的活躍性,要比分析那些不依賴開放調用的程序的活躍性簡單。

開放調用的改造思路

  • 鎖消除;去掉本來要持有的鎖
  • 細粒度鎖;只鎖一小塊,調用其它方法時就不需要鎖了。

資源死鎖的類型

獨佔類型的訪問都可以和加鎖操作類比,看起來就像需要獲得鎖才能訪問。

  • 如果一個任務需要連接兩個數據庫,並且在請求這兩個資源時不會始終遵循相同的順序,那麼線程A可能持有與數據庫D1的連接,並等待與數據庫D2的連接,而線程B持有D2的連接並等待與D1的連接。資源池越大,就越不容易出現這種類型的死鎖。
  • 線程飢餓死鎖。如果某些任務需要等待其它任務的結果,那麼這些任務往往是產生線程飢餓死鎖的主要來源,有界線程池/資源池與相互依賴的任務不能一起使用。

死鎖的避免思路

  • 儘量依賴開放調用。如果一個程序每次至多隻能獲得一個鎖,那麼就不會產生鎖順序死鎖。
  • 如果必須獲取多個鎖,那麼在設計時必須考慮鎖的順序:儘量減少潛在的加鎖交互數量,將獲取鎖時需要遵循的協議寫入正式文檔並始終遵循這些協議。
  • 顯示使用Lock類中的定時tryLock功能來代替內置鎖機制。

定時鎖的優點

  • 當定時鎖失敗時,你並不需要知道失敗的原因。至少你能記錄所發生的失敗,以及關於這次操作的其它有用信息,並通過一種更平緩的方式來重新啓動計算,而不是關閉整個進程。
  • 如果在獲取鎖時超時,那麼可以釋放這個鎖,然後後退並在一段時間後並再次嘗試,從而消除了死鎖發生的條件,使程序恢復過來。(這項技術只有在同時獲取兩個鎖時纔有效,如果在嵌套的方法調用中請求多個鎖,那麼即使你知道已經持有了外層的鎖,也無法釋放它。)

飢餓

當線程由於無法訪問它所需要的資源而不能繼續執行時,就發生了“飢餓”,引發飢餓的最常見資源就是CPU時鐘週期。

問題

我們儘量不要改變線程的優先級。只要改變了線程的優先級,程序的行爲就將與平臺相關

解釋

在Thread API中定義了10個優先級,JVM根據需要將它們映射到操作系統的調度優先級。這種映射與特定平臺相關的,因此在某個操作系統中兩個不同的Java優先級可能被映射到同一個優於級,而在另一個操作系統中則可能被映射到另一個不同的優先級。在某些操作系統中,如果優先級的數量少於10個,那麼有多個java優先級會被映射到同一個優先級。

活鎖

活鎖是另一種形式的活躍性問題,該問題儘管不會阻塞線程,但也不能繼續執行,因爲線程將不斷重複執行相同的操作,而且總會失敗。

過度錯誤恢復代碼

程序捕捉到錯誤消息時,會重新把錯誤消息放入隊列或者直接重複操作,這種過度的錯誤恢復機制,直接導致程序一直在“處理-錯誤-發現-重新處理”的循環中無法跳出
解決辦法就是識別出這種消息,並且跳過處理

不恰當地禮讓

當多個相互協作的線程都對彼此進行響應從而修改各自的狀態,並使得任何一個線程都無法繼續執行時,就發生了活鎖。這就像兩個過於禮貌的人在半路上面對面地相遇,他們彼此都讓出對方的路,然而又在另一條路上相遇。因此他們就這樣反覆地避讓下去。
要解決這種活鎖問題,需要在重試機制中引入隨機性。在併發應用程序中,通過等待隨機長度的時間和回退可以有效避免活鎖的發生。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章