Java產生死鎖的一個簡單例子


什么是死鎖?
所謂死鎖,是指多個進程在運行過程中因爭奪資源而造成的一種僵局,當進程處於這種僵持狀態時,若無外力作用,它們都將無法再向前推進。 因此我們舉個例子來描述,如果此時有一個線程A,按照先鎖a再獲得鎖b的的順序獲得鎖,而在此同時又有另外一個線程B,按照先鎖b再鎖a的順序獲得鎖。如下圖所示:

 

產生死鎖的原因?
可歸結為如下兩點:

a. 競爭資源

系統中的資源可以分為兩類:
可剝奪資源,是指某進程在獲得這類資源后,該資源可以再被其他進程或系統剝奪,CPU和主存均屬於可剝奪性資源;
另一類資源是不可剝奪資源,當系統把這類資源分配給某進程后,再不能強行收回,只能在進程用完后自行釋放,如磁帶機、打印機等。
產生死鎖中的競爭資源之一指的是競爭不可剝奪資源(例如:系統中只有一台打印機,可供進程P1使用,假定P1已占用了打印機,若P2繼續要求打印機打印將阻塞)
產生死鎖中的競爭資源另外一種資源指的是競爭臨時資源(臨時資源包括硬件中斷、信號、消息、緩沖區內的消息等),通常消息通信順序進行不當,則會產生死鎖
b. 進程間推進順序非法

若P1保持了資源R1,P2保持了資源R2,系統處於不安全狀態,因為這兩個進程再向前推進,便可能發生死鎖
例如,當P1運行到P1:Request(R2)時,將因R2已被P2占用而阻塞;當P2運行到P2:Request(R1)時,也將因R1已被P1占用而阻塞,於是發生進程死鎖
死鎖產生的4個必要條件?
產生死鎖的必要條件:

互斥條件:進程要求對所分配的資源進行排它性控制,即在一段時間內某資源僅為一進程所占用。
請求和保持條件:當進程因請求資源而阻塞時,對已獲得的資源保持不放。
不剝奪條件:進程已獲得的資源在未使用完之前,不能剝奪,只能在使用完時由自己釋放。
環路等待條件:在發生死鎖時,必然存在一個進程--資源的環形鏈。
解決死鎖的基本方法
預防死鎖:
資源一次性分配:一次性分配所有資源,這樣就不會再有請求了:(破壞請求條件)
只要有一個資源得不到分配,也不給這個進程分配其他的資源:(破壞請保持條件)
可剝奪資源:即當某進程獲得了部分資源,但得不到其它資源,則釋放已占有的資源(破壞不可剝奪條件)
資源有序分配法:系統給每類資源賦予一個編號,每一個進程按編號遞增的順序請求資源,釋放則相反(破壞環路等待條件)
1、以確定的順序獲得鎖

如果必須獲取多個鎖,那么在設計的時候需要充分考慮不同線程之前獲得鎖的順序。按照上面的例子,兩個線程獲得鎖的時序圖如下:

 

 如果此時把獲得鎖的時序改成:

 

 那么死鎖就永遠不會發生。 針對兩個特定的鎖,開發者可以嘗試按照鎖對象的hashCode值大小的順序,分別獲得兩個鎖,這樣鎖總是會以特定的順序獲得鎖,那么死鎖也不會發生。問題變得更加復雜一些,如果此時有多個線程,都在競爭不同的鎖,簡單按照鎖對象的hashCode進行排序(單純按照hashCode順序排序會出現“環路等待”),可能就無法滿足要求了,這個時候開發者可以使用銀行家算法,所有的鎖都按照特定的順序獲取,同樣可以防止死鎖的發生,該算法在這里就不再贅述了,有興趣的可以自行了解一下。

2、超時放棄

當使用synchronized關鍵詞提供的內置鎖時,只要線程沒有獲得鎖,那么就會永遠等待下去,然而Lock接口提供了boolean tryLock(long time, TimeUnit unit) throws InterruptedException方法,該方法可以按照固定時長等待鎖,因此線程可以在獲取鎖超時以后,主動釋放之前已經獲得的所有的鎖。通過這種方式,也可以很有效地避免死鎖。 還是按照之前的例子,時序圖如下:

 

避免死鎖:
預防死鎖的幾種策略,會嚴重地損害系統性能。因此在避免死鎖時,要施加較弱的限制,從而獲得 較滿意的系統性能。由於在避免死鎖的策略中,允許進程動態地申請資源。因而,系統在進行資源分配之前預先計算資源分配的安全性。若此次分配不會導致系統進入不安全的狀態,則將資源分配給進程;否則,進程等待。其中最具有代表性的避免死鎖算法是銀行家算法。
銀行家算法:首先需要定義狀態和安全狀態的概念。系統的狀態是當前給進程分配的資源情況。因此,狀態包含兩個向量Resource(系統中每種資源的總量)和Available(未分配給進程的每種資源的總量)及兩個矩陣Claim(表示進程對資源的需求)和Allocation(表示當前分配給進程的資源)。安全狀態是指至少有一個資源分配序列不會導致死鎖。當進程請求一組資源時,假設同意該請求,從而改變了系統的狀態,然后確定其結果是否還處於安全狀態。如果是,同意這個請求;如果不是,阻塞該進程知道同意該請求后系統狀態仍然是安全的。
檢測死鎖
首先為每個進程和每個資源指定一個唯一的號碼;
然后建立資源分配表和進程等待表。
解除死鎖:
當發現有進程死鎖后,便應立即把它從死鎖狀態中解脫出來,常采用的方法有:

剝奪資源:從其它進程剝奪足夠數量的資源給死鎖進程,以解除死鎖狀態;
撤消進程:可以直接撤消死鎖進程或撤消代價最小的進程,直至有足夠的資源可用,死鎖狀態.消除為止;所謂代價是指優先級、運行代價、進程的重要性和價值等。
死鎖檢測
1、Jstack命令

jstack是java虛擬機自帶的一種堆棧跟蹤工具。jstack用於打印出給定的java進程ID或core file或遠程調試服務的Java堆棧信息。 Jstack工具可以用於生成java虛擬機當前時刻的線程快照。線程快照是當前java虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖、死循環、請求外部資源導致的長時間等待等。 線程出現停頓的時候通過jstack來查看各個線程的調用堆棧,就可以知道沒有響應的線程到底在后台做什么事情,或者等待什么資源。

2、JConsole工具

Jconsole是JDK自帶的監控工具,在JDK/bin目錄下可以找到。它用於連接正在運行的本地或者遠程的JVM,對運行在Java應用程序的資源消耗和性能進行監控,並畫出大量的圖表,提供強大的可視化界面。而且本身占用的服務器內存很小,甚至可以說幾乎不消耗。 --------------------- 本文來自 江溢jonny 的CSDN 博客 ,全文地址請點擊:https://blog.csdn.net/jonnyhsu_0913/article/details/79633656?utm_source=copy

3、編寫死鎖實例

思路是創建兩個字符串a和b,再創建兩個線程A和B,讓每個線程都用synchronized鎖住字符串(A先鎖a,再去鎖b;B先鎖b,再鎖a),如果A鎖住a,B鎖住b,A就沒辦法鎖住b,B也沒辦法鎖住a,這時就陷入了死鎖。直接貼代碼:

public class DeadLock {
    public static String obj1 = "obj1";
    public static String obj2 = "obj2";
    public static void main(String[] args){
        Thread a = new Thread(new Lock1());
        Thread b = new Thread(new Lock2());
        a.start();
        b.start();
    }    
}
class Lock1 implements Runnable{
    @Override
    public void run(){
        try{
            System.out.println("Lock1 running");
            while(true){
                synchronized(DeadLock.obj1){
                    System.out.println("Lock1 lock obj1");
                    Thread.sleep(3000);//獲取obj1后先等一會兒,讓Lock2有足夠的時間鎖住obj2
                    synchronized(DeadLock.obj2){
                        System.out.println("Lock1 lock obj2");
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
class Lock2 implements Runnable{
    @Override
    public void run(){
        try{
            System.out.println("Lock2 running");
            while(true){
                synchronized(DeadLock.obj2){
                    System.out.println("Lock2 lock obj2");
                    Thread.sleep(3000);
                    synchronized(DeadLock.obj1){
                        System.out.println("Lock2 lock obj1");
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

運行的結果如圖所示:

可以看到,Lock1獲取obj1,Lock2獲取obj2,但是它們都沒有辦法再獲取另外一個obj,因為它們都在等待對方先釋放鎖,這時就是死鎖。

 

如果我們只運行Lock1呢?修改一下main函數,把線程b注釋掉。

public class DeadLock {
    public static String obj1 = "obj1";
    public static String obj2 = "obj2";
    public static void main(String[] args){
        Thread a = new Thread(new Lock1());
        //Thread b = new Thread(new Lock2());
        a.start();
        //b.start();
    }
}
class Lock1 implements Runnable{
    @Override
    public void run(){
        try{
            System.out.println("Lock1 running");
            while(true){
                synchronized(DeadLock.obj1){
                    System.out.println("Lock1 lock obj1");
                    Thread.sleep(3000);
                    synchronized(DeadLock.obj2){
                        System.out.println("Lock1 lock obj2");
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
class Lock2 implements Runnable{
    @Override
    public void run(){
        try{
            System.out.println("Lock2 running");
            while(true){
                synchronized(DeadLock.obj2){
                    System.out.println("Lock2 lock obj2");
                    Thread.sleep(3000);
                    synchronized(DeadLock.obj1){
                        System.out.println("Lock2 lock obj1");
                    }
                }
            }
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

運行結果為:

由於沒有其它線程和Lock1爭奪obj1和obj2,Lock1可以不斷地循環獲取並釋放它們,這時沒有死鎖。


免責聲明!

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



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