【Java面試】以故事的形式教你理解死鎖,如何避免死鎖

什麼是死鎖

先通過一段產生死鎖的代碼來理解死鎖是怎麼產生的。

/**
 * 線程死鎖
 *
 */
public class ThreadDeadkockStudy {

    // 錢
    static Object money = new Object();
    // 貨
    static Object goods = new Object();

    public static void main(String[] args) {

        // 賣家
        new Thread(new Runnable() {
            public void run() {
            	// 賣家拿着貨
                synchronized (goods) {
                    System.out.println("賣家:先錢!");
                    try {
                    	// 我看你怎麼說
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace(); 
                    }
                    // 除非你先給錢
                    synchronized (money) {
                        System.out.println("賣家:合作愉快");
                    }
                }
            }
        }).start();

        // 買家
        new Thread(new Runnable() {
            public void run() {
            	// 買家拿着錢
                synchronized (money) {
                    System.out.println("買家:不行,我先驗驗貨!");
                    try {
                    	// 我也看你怎麼說
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    // 除非你先給我驗驗貨
                    synchronized (goods) {
                        System.out.println("買家:合作愉快");
                    }
                }
            }
        }).start();
    }

}

最後輸出的結果是這樣的,我們發現一直卡在這裏,沒有繼續往下執行了。

賣家:先錢!
買家:不行,我先驗驗貨!

再用比較通俗的語言去解釋這個現象。
黑社會和商人一起到達了約定的交易地點。
商人:“先錢!”
黑社會:“不行,我先驗驗貨!”
商人看不到黑社會的錢,不願意把手裏的貨交出去;黑社會怕商人給的假貨,也不把錢拿出來,就這樣僵住了。

兩個不同的線程,分別擁有各自的資源,並且都想擁有對方持有的資源,但是誰也不願意先妥協,最後陷入了僵局,也就是我們所說的死鎖。


死鎖觸發條件

我們繼續看下面的幾組代碼,看看爲什麼他們爲什麼代碼類似,但是沒有產生死鎖呢。

賣家強買強賣

// 賣家強買強賣
public static void sellerFast() {
    // 賣家
    new Thread(new Runnable() {
        public void run() {
            synchronized (goods) {
                System.out.println("賣家:先錢!");
                synchronized (money) {
                    System.out.println("賣家:合作愉快");
                }
            }
        }
    }).start();

    // 買家
    new Thread(new Runnable() {
        public void run() {
            synchronized (money) {
            	System.out.println("買家:不行,我先驗驗貨!");
                synchronized (goods) {
                    System.out.println("買家:合作愉快");
                }
            }
        }
    }).start();
}

再看看輸出的結果,由於賣家沒有等待買家的迴應,就把買家兜裏的錢給拿走了,並且把貨也硬塞到了自己的兜裏,只能喫啞巴虧了。

賣家:先錢!
賣家:合作愉快
買家:不行,我先驗驗貨!
買家:合作愉快

這個產生的原因是,我們線程初始化和啓動是需要一點時間的,而第一個線程已經執行完了,第二個線程纔剛剛執行到那裏的時候,這個鎖已經被釋放了。

如果兩個線程執行的先後順序對調的情況,我們就可以理解成是:買家看賣家不在店裏,但是看到了自己想買的東西,沒等賣家回來就把錢放在了櫃檯上,並且留下了個字條:我看你不在,我把貨拿走了,錢我放在櫃檯了!
最後他們成功的完成了這筆交易。

  1. 互斥條件:進程對於所分配到的資源具有排它性,即一個資源只能被一個進程佔用,直到被該進程釋放 。
  2. 請求和保持條件:一個進程因請求被佔用資源而發生阻塞時,對已獲得的資源保持不放。
  3. 不剝奪條件:任何一個資源在沒被該進程釋放之前,任何其他進程都無法對他剝奪佔用。
  4. 循環等待條件:當發生死鎖時,所等待的進程必定會形成一個環路(類似於死循環),造成永久阻塞。

我們這裏沒有滿足第二條,因爲當我們想要取用該資源的時候,資源已經被提早釋放了,所以就沒有造成死鎖。


如何避免死鎖

在生活中,一般造成死鎖都是由於溝通問題導致的,比如跨行轉賬也可能出現死鎖的情況,但是這完全是可以被避免的。
死鎖
如果沒有中間機構,我們也可以通過自行妥協的方式,規定一個比較合理的順序,來避免死鎖的誕生。

加鎖順序: 當多個線程需要相同的一些鎖,但是按照不同的順序加鎖,死鎖就很容易發生。如果能確保所有的線程都是按照相同的順序獲得鎖,那麼死鎖就不會發生。當然這種方式需要你事先知道所有可能會用到的鎖,然而總有些時候是無法預知的。
加鎖時限: 加上一個超時時間,若一個線程沒有在給定的時限內成功獲得所有需要的鎖,則會進行回退並釋放所有已經獲得的鎖,然後等待一段隨機的時間再重試。但是如果有非常多的線程同一時間去競爭同一批資源,就算有超時和回退機制,還是可能會導致這些線程重複地嘗試但卻始終得不到鎖。
死鎖檢測: 死鎖檢測即每當一個線程獲得了鎖,會在線程和鎖相關的數據結構中(map、graph等等)將其記下。除此之外,每當有線程請求鎖,也需要記錄在這個數據結構中。死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖並且鎖超時也不可行的場景。


參考資料

什麼是線程死鎖?如何避免死鎖?

線程死鎖

文章中出現的任何錯誤歡迎指正,共同進步!

最後做個小小廣告,有對WEB開發和網絡安全感興趣的,可以加羣一起學習和交流!

交流羣
QQ:425343603

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