什么是死鎖?怎么樣才能預防死鎖?如何定位死鎖?


什么是死鎖:

是指兩個或兩個以上的進程在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖。

舉個例子:

A 和 B 去按摩洗腳,都想在洗腳的時候,同時順便做個頭部按摩,13 技師擅長足底按摩,14 擅長頭部按摩。

這個時候 A 先搶到 14,B 先搶到 13,兩個人都想同時洗腳和頭部按摩,於是就互不相讓,揚言我死也不讓你,這樣的話,A 搶到 14,想要 13,B 搶到 13,想要 14,在這個想同時洗腳和頭部按摩的事情上 A 和 B 就產生了死鎖。

怎么解決這個問題呢?

第一種,假如這個時候,來了個 15,剛好也是擅長頭部按摩的,A 又沒有兩個腦袋,自然就歸了 B,於是 B 就美滋滋的洗腳和做頭部按摩,剩下 A 在旁邊氣鼓鼓的,這個時候死鎖這種情況就被打破了,不存在了。

第二種,C 出場了,用武力強迫 A 和 B,必須先做洗腳,再頭部按摩,這種情況下,A 和 B 誰先搶到 13,誰就可以進行下去,另外一個沒搶到的,就等着,這種情況下,也不會產生死鎖。

總結一下:

死鎖是必然發生在多操作者(M>=2 個)情況下,爭奪多個資源(N>=2 個,且 N<=M)才會發生這種情況。很明顯,單線程自然不會有死鎖,只有 B 一個去,不要 2 個,打十個都沒問題;單資源呢?只有 13,A 和 B 也只會產生激烈競爭,打得不可開交,誰搶到就是誰的,但不會產生死鎖。同時,死鎖還有一個重要的要求,爭奪資源的順序不對,如果爭奪資源的順序是一樣的,也不會產生死鎖。

死鎖的發生下四個必要條件:

1) 互斥條件:指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程占用。如果此時還有其它進程請求資源,則請求者只能等待,直至占有資源的進程用畢釋放。
2) 請求和保持條件:指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程占有,此時請求進程阻塞,但又對自己已獲得的其它資源保持不放。
3) 不剝奪條件:指進程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
4) 環路等待條件:指在發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合{P0,P1,P2,···,Pn}中的 P0 正在等待一個 P1 占用的資源;P1正在等待 P2 占用的資源,……,Pn 正在等待已被 P0 占用的資源。

  理解了死鎖的原因,尤其是產生死鎖的四個必要條件,就可以最大可能地避免、預防和解除死鎖。

如何有效預防死鎖的發生:

  只要打破四個必要條件之一就可以了

打破互斥條件:改造獨占性資源為虛擬資源,大部分資源已無法改造。

打破不可搶占條件:當一進程占有一獨占性資源后又申請一獨占性資源而無法滿足,則退出原占有的資源。

打破占有且申請條件:采用資源預先分配策略,即進程運行前申請全部資源,滿足則運行,不然就等待,這樣就不會占有且申請。

打破循環等待條件:實現資源有序分配策略,對所有設備實現分類編號,所有進程只能采用按序號遞增的形式申請資源。

避免死鎖常見的算法:

  有有序資源分配法、銀行家算法。

現象、危害和解決

在我們 IT 世界有沒有存在死鎖的情況,有:數據庫里多事務而且要同時操作多個表的情況下。所以數據庫設計的時候就考慮到了檢測死鎖和從死鎖中恢復的機制。比如 oracle 提供了檢測和處理死鎖的語句,而 mysql 也提供了“循環依賴檢測的機制”

  

現象

簡單順序死鎖示例:

/**
 * @ClassName NormalDeadLock
 * @Description TODO 簡單順序死鎖測試
 * @Date 2020/5/10 16:07
 **/
public class NormalDeadLock {
    private static Object valueFirst = new Object();//第一個鎖
    private static Object valueSecond = new Object();//第二個鎖

    //先拿第一個鎖,再拿第二個鎖
    private static void fisrtToSecond() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (valueFirst) {
            System.out.println(threadName + " get 1st");
            Thread.sleep(100);
            synchronized (valueSecond) {
                System.out.println(threadName + " get 2nd");
            }
        }
    }

    //先拿第二個鎖,再拿第一個鎖
    private static void SecondToFisrt() throws InterruptedException {
        String threadName = Thread.currentThread().getName();
        synchronized (valueSecond) {
            System.out.println(threadName + " get 2nd");
            Thread.sleep(100);
            synchronized (valueFirst) {
                System.out.println(threadName + " get 1st");
            }
        }
    }

    private static class TestThread extends Thread {

        private String name;

        public TestThread(String name) {
            this.name = name;
        }

        public void run() {
            Thread.currentThread().setName(name);
            try {
                SecondToFisrt();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Thread.currentThread().setName("TestDeadLock");
        TestThread testThread = new TestThread("SubTestThread");
        testThread.start();
        try {
            fisrtToSecond();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

  簡單順序死鎖解決方案:在鎖的時,兩個方法都 先拿第一個鎖,再拿第二個鎖

動態順序死鎖及解決方案代碼示例

  • 用戶賬戶的實體類
  • 銀行轉賬動作接口
  • 不安全的轉賬動作的實現(動態順序死鎖)
  • 不會產生死鎖的安全轉賬一(解決動態順序死鎖方案一)
  • 不會產生死鎖的安全轉賬二(解決動態順序死鎖方案二)
  • main函數測試類

用戶賬戶的實體類代碼示例

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 類說明:用戶賬戶的實體類
 */
public class UserAccount {
    private final String name;//賬戶名稱
    private int money;//賬戶余額

    private final Lock lock = new ReentrantLock();

    public Lock getLock() {
        return lock;
    }

    public UserAccount(String name, int amount) {
        this.name = name;
        this.money = amount;
    }

    public String getName() {
        return name;
    }

    public int getAmount() {
        return money;
    }

    @Override
    public String toString() {
        return "UserAccount{" + "name='" + name + '\'' + ", money=" + money + '}';
    }

    //轉入資金
    public void addMoney(int amount) {
        money = money + amount;
    }

    //轉出資金
    public void flyMoney(int amount) {
        money = money - amount;
    }
}

銀行轉賬動作接口代碼示例

/**
 * 類說明:銀行轉賬動作接口
 */
public interface ITransfer {
    void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException;
}

不安全的轉賬動作的實現(動態順序死鎖)代碼示例

/**
 * 類說明:不安全的轉賬動作的實現(動態順序死鎖)
 */
public class TrasnferAccount implements ITransfer {
    /**
     * @param from--轉出對象
     * @param to--轉入對象
     * @param amount--賬戶余額
     * @throws InterruptedException
     */
    @Override
    public void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException {
        synchronized (from) {
            System.out.println(Thread.currentThread().getName() + " get" + from.getName());
            Thread.sleep(100);
            synchronized (to) {
                System.out.println(Thread.currentThread().getName() + " get" + to.getName());
                from.flyMoney(amount);
                to.addMoney(amount);
            }
        }
    }
}

不會產生死鎖的安全轉賬一(解決動態順序死鎖方案一)代碼示例

/**
 * 類說明:不會產生死鎖的安全轉賬一(解決動態順序死鎖方案一)
 * 原理:
 * 通過比較hash值決定線程的執行順序
 * 具體實現思路:
 * 1. 獲取各個對象的hash值
 * 2.1 若hash值小則先鎖對象一,再鎖對象二
 * 2.2 若hash值大則先鎖對象二,再鎖對象一
 * 2.3 若hash值相同,則在第三把鎖的基礎上,再同時加鎖(Ps:出現此情況的幾率特別小,所以在保證線程的安全基礎上,不必太在意這一點性能)
 */
public class SafeOperate implements ITransfer {

    private static Object tieLock = new Object();//第三把鎖

    /**
     * @param from--轉出對象
     * @param to--轉入對象
     * @param amount--賬戶余額
     * @throws InterruptedException
     */
    @Override
    public void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException {
        // 獲取對象的hash值
        int fromHash = System.identityHashCode(from);
        int toHash = System.identityHashCode(to);
        // 通過比較hash值決定線程的執行順序
        if (fromHash < toHash) {
            synchronized (from) {
                System.out.println(Thread.currentThread().getName() + " get " + from.getName());
                Thread.sleep(100);
                synchronized (to) {
                    System.out.println(Thread.currentThread().getName() + " get " + to.getName());
                    from.flyMoney(amount);
                    to.addMoney(amount);
                    System.out.println(from);
                    System.out.println(to);
                }
            }
        } else if (toHash < fromHash) {
            synchronized (to) {
                System.out.println(Thread.currentThread().getName() + " get" + to.getName());
                Thread.sleep(100);
                synchronized (from) {
                    System.out.println(Thread.currentThread().getName() + " get" + from.getName());
                    from.flyMoney(amount);
                    to.addMoney(amount);
                    System.out.println(from);
                    System.out.println(to);
                }
            }
        } else {
            // 若hash值相同,則在第三把鎖的基礎上,再同時加鎖(Ps:出現此情況的幾率特別小,所以在保證線程的安全基礎上,不必太在意這一點性能)
            synchronized (tieLock) {
                synchronized (from) {
                    synchronized (to) {
                        from.flyMoney(amount);
                        to.addMoney(amount);
                    }
                }
            }
        }
    }
}

不會產生死鎖的安全轉賬二(解決動態順序死鎖方案二)代碼示例

import java.util.Random;

/**
 * 類說明:不會產生死鎖的安全轉賬二(解決動態順序死鎖方案二)
 * 原理:
 * 獲取所有的鎖后再繼續執行
 * 具體實現思路:
 * 1.若未獲取第一把鎖則while無限循環
 * 2.獲取第一把鎖后,若無法獲取第二把鎖則釋放第一把鎖繼續循環
 * 3.獲取第一把鎖,同時獲取第二把鎖時執行任務
 * 4.結束
 */
public class SafeOperateToo implements ITransfer {

    /**
     * @param from--轉出對象
     * @param to--轉入對象
     * @param amount--賬戶余額
     * @throws InterruptedException
     */
    @Override
    public void transfer(UserAccount from, UserAccount to, int amount) throws InterruptedException {
        // 隨機數:避免活鎖
        Random r = new Random();
        while (true) {
            if (from.getLock().tryLock()) {
                System.out.println(Thread.currentThread().getName() + " get" + from.getName());
                try {
                    if (to.getLock().tryLock()) {
                        try {
                            System.out.println(Thread.currentThread().getName() + " get" + to.getName());
                            from.flyMoney(amount);
                            to.addMoney(amount);
                            System.out.println(from);
                            System.out.println(to);
                            // 執行完任務后退出
                            break;
                        } finally {
                            to.getLock().unlock();
                        }
                    }
                } finally {
                    from.getLock().unlock();
                }
            }
            //錯開時間,避免活鎖
            Thread.sleep(r.nextInt(5));
        }
    }
}

main函數測試類代碼示例

/**
 * 類說明:模擬支付公司轉賬的動作
 * 測試類
 */
public class PayCompany {

    /*執行轉賬動作的線程*/
    private static class TransferThread extends Thread {

        private String name;
        private UserAccount from;
        private UserAccount to;
        private int amount;
        private ITransfer transfer;

        public TransferThread(String name, UserAccount from, UserAccount to, int amount, ITransfer transfer) {
            this.name = name;
            this.from = from;
            this.to = to;
            this.amount = amount;
            this.transfer = transfer;
        }

        public void run() {
            Thread.currentThread().setName(name);
            try {
                transfer.transfer(from, to, amount);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    public static void main(String[] args) {
        PayCompany payCompany = new PayCompany();
        // 對象一
        UserAccount zhangsan = new UserAccount("zhangsan", 20000);
        // 對象二
        UserAccount lisi = new UserAccount("lisi", 20000);
        // 線程不安全:TrasnferAccount
        //ITransfer transfer = new TrasnferAccount();
        // 線程安全:SafeOperate
        //ITransfer transfer = new SafeOperate();
        // 線程安全:SafeOperateToo
        ITransfer transfer = new SafeOperateToo();
        // 張三轉李四
        TransferThread zhangsanToLisi = new TransferThread("zhangsanToLisi", zhangsan, lisi, 2000, transfer);
        // 李四轉張三
        TransferThread lisiToZhangsan = new TransferThread("lisiToZhangsan", lisi, zhangsan, 4000, transfer);
        // 線程開始執行
        zhangsanToLisi.start();
        lisiToZhangsan.start();

    }

}

危害

1、線程不工作了,但是整個程序還是活着的

2、沒有任何的異常信息可以供我們檢查。

3、一旦程序發生了發生了死鎖,是沒有任何的辦法恢復的,只能重啟程序,對生產平台的程序來說,這是個很嚴重的問題。

實際工作中的死鎖

時間不定,不是每次必現;一旦出現沒有任何異常信息,只知道這個應用的所有業務越來越慢,最后停止服務。。。。

若發生死鎖,如何定位死鎖?

1.打開cmd

2.cmd切換路徑到jdk的bin目錄下

 

 3.檢查是否有死鎖信息,輸入:jps - 

回車:若是有死鎖則會打印出死鎖的具體信息,若是沒有則會出現以下`數據

 4.定位死鎖的具體信息,輸入:jstack  序號(jstack+空格+死鎖線程的Id)

回車:即會打印死鎖線程的具體信息

定位死鎖示例:

  cmd 定位死鎖:

   若有IDEA,左側的小照相機圖標也可定位死鎖:

死鎖解決方案

關鍵是保證拿鎖的順序一致

兩種解決方式(參考以上代碼↑↑↑)

1、內部通過順序比較,確定拿鎖的順序;

2、采用嘗試拿鎖的機制。

其他安全問題

活鎖(參考以上代碼)

兩個線程在嘗試拿鎖的機制中,發生多個線程之間互相謙讓,不斷發生同一個線程總是拿到同一把鎖,在嘗試拿另一把鎖時因為拿不到,而將本來已經持有的鎖釋放的過程。

解決辦法:

每個線程休眠隨機數,錯開拿鎖的時間。

線程不工作了,但是整個程序還是活着的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM