Java中的死鎖,你真的瞭解嗎

一、死鎖的定義

  多線程以及多進程改善了系統資源的利用率並提高了系統 的處理能力。然而,併發執行也帶來了新的問題——死鎖。所謂死鎖是指多個線程因競爭資源而造成的一種僵局(互相等待),若無外力作用,這些進程都將無法向前推進。
  所謂死鎖是指兩個或兩個以上的線程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。

  下面我們通過實例來了解死鎖的現象:
   先看生活中的一個實例,兩個人面對面過獨木橋,甲和乙都已經在橋上走了一段距離,即佔用了橋的資源,甲如果想通過獨木橋的話,乙必須退出橋面讓出橋的資源,讓甲通過,但是乙不服,爲什麼讓我先退出去,我還想先過去呢,於是就僵持不下,導致誰也過不了橋,這就是死鎖。

  在計算機系統中也存在類似的情況。例如,某計算機系統中只有一臺打印機和一臺輸入 設備,進程P1正佔用輸入設備,同時又提出使用打印機的請求,但此時打印機正被進程P2 所佔用,而P2在未釋放打印機之前,又提出請求使用正被P1佔用着的輸入設備。這樣兩個進程相互無休止地等待下去,均無法繼續執行,此時兩個進程陷入死鎖狀態。

二、死鎖產生的原因

  1、系統資源的競爭
  通常系統中所擁有的不可剝奪資源,其數量不足以滿足多個進程運行的需要,使得進程在運行過程中,會因爭奪資源而陷入僵局,如磁帶機、打印機等。只有多個進程在對不可剝奪資源的競爭纔可能產生死鎖,對可剝奪資源的競爭是不會引起死鎖的。

  2、進程推進順序非法
  進程在運行過程中,請求和釋放資源的順序不當,也同樣會導致死鎖。例如,併發進程 P1、P2分別保持了資源R1、R2,而進程P1申請資源R2,進程P2申請資源R1時,兩者都會因爲所需資源被佔用而阻塞。
  Java中死鎖最簡單的情況是,一個線程T1持有鎖L1並且申請獲得鎖L2,而另一個線程T2持有鎖L2並且申請獲得鎖L1,因爲默認的鎖申請操作都是阻塞的,所以線程T1和T2永遠被阻塞了。導致了死鎖。這是最容易理解也是最簡單的死鎖的形式。但是實際環境中的死鎖往往比這個複雜的多。可能會有多個線程形成了一個死鎖的環路,比如:線程T1持有鎖L1並且申請獲得鎖L2,而線程T2持有鎖L2並且申請獲得鎖L3,而線程T3持有鎖L3並且申請獲得鎖L1,這樣導致了一個鎖依賴的環路:T1依賴T2的鎖L2,T2依賴T3的鎖L3,而T3依賴T1的鎖L1。從而導致了死鎖。
  從以上兩個例子中,我們可以得出結論,產生死鎖可能性的最根本原因是:線程在獲得一個鎖L1的情況下再去申請另外一個鎖L2,也就是鎖L1想要包含了鎖L2,也就是說在獲得了鎖L1,並且沒有釋放鎖L1的情況下,又去申請獲得鎖L2,這個是產生死鎖的最根本原因。另一個原因是默認的鎖申請操作是阻塞的。
這裏寫圖片描述

  3、死鎖產生的必要條件:
  產生死鎖必須同時滿足以下四個條件,只要其中任一條件不成立,死鎖就不會發生。
  (1)互斥條件:進程要求對所分配的資源(如打印機)進行排他性控制,即在一段時間內某資源僅爲一個進程所佔有。此時若有其他進程請求該資源,則請求進程只能等待。
  (2)不剝奪條件:進程所獲得的資源在未使用完畢之前,不能被其他進程強行奪走,即只能由獲得該資源的進程自己來釋放(只能是主動釋放)。
  (3)請求和保持條件:進程已經保持了至少一個資源,但又提出了新的資源請求,而該資源已被其他進程佔有,此時請求進程被阻塞,但對自己已獲得的資源保持不放。
  (4)循環等待條件:存在一種進程資源的循環等待鏈,鏈中每一個進程已獲得的資源同時被鏈中下一個進程所請求。即存在一個處於等待狀態的進程集合{Pl, P2, …, pn},其中Pi等 待的資源被P(i+1)佔有(i=0, 1, …, n-1),Pn等待的資源被P0佔有,如圖1所示。
  直觀上看,循環等待條件似乎和死鎖的定義一樣,其實不然。按死鎖定義構成等待環所 要求的條件更嚴,它要求Pi等待的資源必須由P(i+1)來滿足,而循環等待條件則無此限制。 例如,系統中有兩臺輸出設備,P0佔有一臺,PK佔有另一臺,且K不屬於集合{0, 1, …, n}。Pn等待一臺輸出設備,它可以從P0獲得,也可能從PK獲得。因此,雖然Pn、P0和其他 一些進程形成了循環等待圈,但PK不在圈內,若PK釋放了輸出設備,則可打破循環等待, 如圖2-16所示。因此循環等待只是死鎖的必要條件。
這裏寫圖片描述

  資源分配圖含圈而系統又不一定有死鎖的原因是同類資源數大於1。但若系統中每類資 源都只有一個資源,則資源分配圖含圈就變成了系統出現死鎖的充分必要條件。下面再來通俗的解釋一下死鎖發生時的條件:
(1)互斥條件:一個資源每次只能被一個進程使用。獨木橋每次只能通過一個人。
(2)請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。乙不退出橋面,甲也不退出橋面。
(3)不剝奪條件: 進程已獲得的資源,在未使用完之前,不能強行剝奪。甲不能強制乙退出橋面,乙也不能強制甲退出橋面。
(4)循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。如果乙不退出橋面,甲不能通過,甲不退出橋面,乙不能通過。

三、死鎖實例

例1:
package com.demo.test;

/**
 * 一個簡單的死鎖類
 * t1先運行,這個時候flag==true,先鎖定obj1,然後睡眠1秒鐘
 * 而t1在睡眠的時候,另一個線程t2啓動,flag==false,先鎖定obj2,然後也睡眠1秒鐘
 * t1睡眠結束後需要鎖定obj2才能繼續執行,而此時obj2已被t2鎖定
 * t2睡眠結束後需要鎖定obj1才能繼續執行,而此時obj1已被t1鎖定
 * t1、t2相互等待,都需要得到對方鎖定的資源才能繼續執行,從而死鎖。 
 */
public class DeadLock implements Runnable{
    
    private static Object obj1 = new Object();
    private static Object obj2 = new Object();
    private boolean flag;
    
    public DeadLock(boolean flag){
        this.flag = flag;
    }
    
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName() + "運行");
        
        if(flag){
            synchronized(obj1){
                System.out.println(Thread.currentThread().getName() + "已經鎖住obj1");
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                synchronized(obj2){
                    // 執行不到這裏
                    System.out.println("1秒鐘後,"+Thread.currentThread().getName()
                                + "鎖住obj2");
                }
            }
        }else{
            synchronized(obj2){
                System.out.println(Thread.currentThread().getName() + "已經鎖住obj2");
                try {  
                    Thread.sleep(1000);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                synchronized(obj1){
                    // 執行不到這裏
                    System.out.println("1秒鐘後,"+Thread.currentThread().getName()
                                + "鎖住obj1");
                }
            }
        }
    }

}
package com.demo.test;

public class DeadLockTest {

     public static void main(String[] args) {
         
         Thread t1 = new Thread(new DeadLock(true), "線程1");
         Thread t2 = new Thread(new DeadLock(false), "線程2");

         t1.start();
         t2.start();
    }
}

運行結果:

線程1運行
線程1已經鎖住obj1
線程2運行
線程2已經鎖住obj2

  線程1鎖住了obj1(甲佔有橋的一部分資源),線程2鎖住了obj2(乙佔有橋的一部分資源),線程1企圖鎖住obj2(甲讓乙退出橋面,乙不從),進入阻塞,線程2企圖鎖住obj1(乙讓甲退出橋面,甲不從),進入阻塞,死鎖了。
   從這個例子也可以反映出,死鎖是因爲多線程訪問共享資源,由於訪問的順序不當所造成的,通常是一個線程鎖定了一個資源A,而又想去鎖定資源B;在另一個線程中,鎖定了資源B,而又想去鎖定資源A以完成自身的操作,兩個線程都想得到對方的資源,而不願釋放自己的資源,造成兩個線程都在等待,而無法執行的情況。

package com.demo.test;

public class SyncThread implements Runnable{
    
    private Object obj1;
    private Object obj2;
 
    public SyncThread(Object o1, Object o2){
        this.obj1=o1;
        this.obj2=o2;
    }
    
    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        synchronized (obj1) {
            System.out.println(name + " acquired lock on "+obj1);
            work();
            synchronized (obj2) {
                System.out.println("After, "+name + " acquired lock on "+obj2);
                work();
            }
            System.out.println(name + " released lock on "+obj2);
        }
        System.out.println(name + " released lock on "+obj1);
        System.out.println(name + " finished execution.");
    }
    
    private void work() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
package com.demo.test;

public class ThreadDeadTest {

    public static void main(String[] args) throws InterruptedException {
        Object obj1 = new Object();
        Object obj2 = new Object();
        Object obj3 = new Object();
 
        Thread t1 = new Thread(new SyncThread(obj1, obj2), "t1");
        Thread t2 = new Thread(new SyncThread(obj2, obj3), "t2");
        Thread t3 = new Thread(new SyncThread(obj3, obj1), "t3");
 
        t1.start();
        Thread.sleep(1000);
        t2.start();
        Thread.sleep(1000);
        t3.start();
 
    }
}

運行結果:

t1 acquired lock on java.lang.Object@5e1077
t2 acquired lock on java.lang.Object@1db05b2
t3 acquired lock on java.lang.Object@181ed9e

  在這個例子中,形成了一個鎖依賴的環路。以t1爲例,它先將第一個對象鎖住,但是當它試着向第二個對象獲取鎖時,它就會進入等待狀態,因爲第二個對象已經被另一個線程鎖住了。這樣以此類推,t1依賴t2鎖住的對象obj2,t2依賴t3鎖住的對象obj3,而t3依賴t1鎖住的對象obj1,從而導致了死鎖。在線程引起死鎖的過程中,就形成了一個依賴於資源的循環。

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