死鎖

當兩個或多個執行緒的某個組的每個成員必須等待其中一個成員執行某些操作(例如,釋放鎖定)才能繼續之前發生死鎖。沒有干預,執行緒將永遠等待。

容易出現死鎖設計的虛擬碼示例如下:

thread_1 {
    acquire(A)
    ...
    acquire(B)
    ...
    release(A, B)
}

thread_2 {
    acquire(B)
    ...
    acquire(A)
    ...
    release(A, B)
}

thread_1 獲得 A 時,可能會發生僵局,但還沒有 B,而 thread_2 已經獲得了 B,但不是 A。如下圖所示,兩個執行緒將永遠等待。 死鎖圖

如何避免死鎖

作為一般經驗法則,最小化鎖的使用,並最小化鎖和解鎖之間的程式碼。

以相同順序獲取鎖

thread_2 的重新設計解決了這個問題:

thread_2 {
    acquire(A)
    ...
    acquire(B)
    ...
    release(A, B)
}

兩個執行緒以相同的順序獲取資源,從而避免死鎖。

此解決方案稱為資源層次結構解決方案。Dijkstra 提議將其作為餐飲哲學家問題的解決方案。

有時即使你指定了鎖定獲取的嚴格順序,也可以在執行時使這種靜態鎖定獲取順序動態化。

考慮以下程式碼:

void doCriticalTask(Object A, Object B){
     acquire(A){
        acquire(B){
            
        }
    }
}

這裡即使鎖獲取順序看起來是安全的,當 thread_1 訪問此方法時也會導致死鎖,例如,Object_1 作為引數 A,Object_2 作為引數 B,thread_2 按相反的順序執行,即 Object_2 作為引數 A,Object_1 作為引數 B.

在這種情況下,最好使用 Object_1 和 Object_2 通過某種計算得到一些唯一條件,例如使用兩個物件的雜湊碼,因此每當不同的執行緒以任何引數順序進入該方法時,每次該唯一條件將匯出鎖定獲取訂單。

例如,Say Object 有一些唯一的金鑰,例如 Account 物件的 accountNumber。

void doCriticalTask(Object A, Object B){
    int uniqueA = A.getAccntNumber();
    int uniqueB = B.getAccntNumber();
    if(uniqueA > uniqueB){
         acquire(B){
            acquire(A){
                
            }
        }
    }else {
         acquire(A){
            acquire(B){
                
            }
        }
    }
}