1. 程式人生 > >關於記憶體安全,執行緒安全,死鎖(中)

關於記憶體安全,執行緒安全,死鎖(中)

接上,死鎖問題

1.原因

定義之前已經闡述,這裡先上一個死鎖最簡單的例子:

//執行緒1:
    public void leftRight() {
        // 得到left鎖
        synchronized (left) {
            // 得到right鎖
            synchronized (right) {
                doSomething();
            }
        }
    }


//執行緒2:
    public void rightLeft() {
        // 得到right鎖
        synchronized (right) {
            // 得到left鎖
            synchronized (left) {
                doSomethingElse();
            }
        }
    }


可以看到,1執行緒鎖住left,現在要right,不給就等,等的時候left也不解鎖;2執行緒同時開始執行,於是鎖住right,要left,不給也等,等的時候right也不解鎖。於是誰都進行不下去。

結合例子看死鎖條件:

1.互斥:即獨佔,synchronized鎖是表面原因,一個人佔用了就不給其他人用,否則1執行緒就不需要等待left了,直接拿來用就行了。根本原因是被鎖的東西的屬性,屬性要求不能被同時共享,如印表機,如對賬戶的修改。

2.不剝奪:即執行緒1佔用了left之後,不會因為你2來了就會給你,只有我執行完了才釋放,2也是這麼想的。

3.保持和請求:如果單單不剝奪,比如執行緒1只能佔用一個執行緒,現在佔著left,就夠用了,不會要求right也就沒事了,但還是必須要吃著碗裡的看著鍋裡的才會導致死鎖。

4.迴圈等待:對資源的需求鏈要能構成閉環,死鎖不是卡住,錯誤原因必須要是來自邏輯的謬誤而非印表機壞了:2欠了1的錢,3欠了2的錢,3欠了1的錢,大家在內部越借越多,全是次級信貸,借了一圈到了該還錢的都傻眼了,金融危機爆發;如果123都是找我要錢,我就是不給,雖然後果是一樣的,但不能稱之為死鎖。

 

2.如何避免

其實標題應該是程式設計中如何避免死鎖,有哪些避免的措施,像銀行家演算法這類作業系統層面的東西暫不考慮

1.保證上鎖順序

舉個例子,設執行緒1,2用到的資源並集是ACD(即兩個都用的資源),當執行緒1依次對ACD上鎖時,我們發現,如果2對資源中A,C,D,無論何時,無論和其他資源什麼順序,只要保證這三個資源的上鎖順序是依次的,就可以避免死鎖。其實相當於ACD三個資源合併了。

舉一個常用的轉賬例子:

從賬戶from轉到賬戶to,顯然這兩個賬戶都要上鎖,如果這麼寫:

//先鎖轉出賬戶
synchronized (fromAccount) { 
//再鎖轉入賬戶
        synchronized (toAccount) {
            //查詢是否有餘額
            if (fromAccount.getBalance().compareTo(amount) < 0)
                throw new InsufficientFundsException();
            else {
                //轉出賬戶減錢,轉入的加錢
                fromAccount.debit(amount);
                toAccount.credit(amount);
            }
        }
    }

看起來是很有道理的,所有人都先鎖轉出賬戶,再鎖轉入賬戶,完美。

其實這幫助我們認識到一個常見的順序上鎖誤區:當我們在談論A,C,D時,我們在談論什麼?

這時剛剛那個轉賬的方法頭:

public static void transferMoney(Account fromAccount,Account toAccount,DollarAmount amount)

現在有這樣一個場景:我在淘寶上買東西,給馬雲10塊,馬雲分紅,給我1億:

//taobao買東西
public static void transferMoney(Account myAccount,Account mayunAccount,DollarAmount 10)
//分紅
public static void transferMoney(Account mayunAccount,Account myAccount,DollarAmount 100000000)

執行緒1會先鎖我的賬戶並要馬雲賬戶鎖,執行緒2會先鎖馬雲賬戶並要我賬戶鎖,仍然死鎖了。所以from和to只是形參而已,不是資源的肉身。引入兩個方法的概念:

S1.hashCode:根據S1的字元序列計算其雜湊值;

S1.identityHashCode:根據S1的記憶體地址來計算雜湊值。

記憶體地址可以在一些情況下確定資源的肉身:

public class InduceLockOrder {
    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.identityHashCode(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();
                    }
                }
            }
        }
    }}

總結來說,當有100個資源要共享時,要為他們每個人安排在這個獨一無二的標號,不管每個執行緒申請哪些,都按照從小到大(一個順序,都是大到小也可以)的順序加鎖。

2.時限鎖

打破了保持和請求的條件,拿不到不頭鐵,不硬等。例如使用可中斷鎖trylock():沒獲取返回false,

boolean tryLock(long time, TimeUnit unit):在設定等待時間內沒有獲取則返回false。

3.檢測

可以用工具對程式碼進行審查,參考資料:https://blog.csdn.net/abc86319253/article/details/49534225

原理大致是在一個執行緒獲取鎖時,會有記錄在一個數據結構中,通過遍歷這個結構形成關係圖,檢視對資源的佔有是否滿足死鎖。

我不知道多少人會由此想到圖靈的停機問題:

圖靈在1936年證明這樣一個演算法是不存在的:該過程以一個計算機程式以及該程式的一個輸入作為輸入,並判斷該過程在給定輸入執行時是否最終能停止。

???   檢測工具不就是能判斷輸入程式是否會終止咩?

這段部落格中的話著實不嚴謹,真正的問題描述不是“一個程式作為輸入”而是“任意一個程式作為輸入”,它的證明方法也是將自身作為程式帶入自身。(話說,把程式碼審查的程式碼帶入程式碼審查會發生什麼?----之後開一個坑,分享看《論可計算數》學到的知識)