Java多線程產生死鎖的4個必要條件?如何避免死鎖?

通常形成死鎖必須同時知足如下4個條件:
1. 互斥條件:線程使用的資源必須至少有一個是不能共享的。即在一段時間內,一個資源只能被一個進程佔用,直到被該進程釋放。
2. 請求與保持條件:指的是進程至少有一個資源,但又提出了新的資源請求,而該資源已被其它線程佔有,此時請求進程阻塞,但又對本身得到的其它資源保持不釋放。
3. 不可搶佔條件:指的是進程已得到資源,在未使用完以前,不能被搶佔,只能在使用完時由本身釋放。
4. 循環等待條件:第一個線程等待其餘的線程釋放資源。後者又在等待第一個線程釋放資源。
由於要產生死鎖,這4個條件必須同時知足。所以要防止死鎖的話,只須要破壞其中一個條件便可。java

如何避免死鎖?

答:mysql

指定獲取鎖的順序,舉例:web

好比某個線程只有得到 A 鎖和 B 鎖才能對某資源進行操做,在多線程條件下,如何避免死鎖?
得到鎖的順序是必定的,好比規定只有得到 A 鎖的線程纔有資格獲取 B 鎖,按順序獲取鎖就能夠避免死鎖。算法

那麼開發過程當中咱們應該怎麼對待死鎖這個問題呢?

  1. 預防死鎖。

這是一種較簡單和直觀的事先預防的方法。方法是經過設置某些限制條件,去破壞產生死鎖的四個必要條件中的一個或者幾個,來預防發生死鎖。預防死鎖是一種較易實現的方法,已被普遍使用。可是因爲所施加的限制條件每每太嚴格,可能會致使系統資源利用率和系統吞吐量下降。sql

  1. 避免死鎖。

該方法一樣是屬於事先預防的策略,但它並不須事先採起各類限制措施去破壞產生死鎖的的四個必要條件,而是在資源的動態分配過程當中,用某種方法去防止系統進入不安全狀態,從而避免發生死鎖。數據庫

3)檢測死鎖。安全

這種方法並不須事先採起任何限制性措施,也沒必要檢查系統是否已經進入不安全區,此方法容許系統在運行過程當中發生死鎖。但可經過系統所設置的檢測機構,及時地檢測出死鎖的發生,並精確地肯定與死鎖有關的進程和資源,而後採起適當措施,從系統中將已發生的死鎖清除掉。數據結構

4)解除死鎖。多線程

這是與檢測死鎖相配套的一種措施。當檢測到系統中已發生死鎖時,須將進程從死鎖狀態中解脫出來。經常使用的實施方法是撤銷或掛起一些進程,以便回收一些資 源,再將這些資源分配給已處於阻塞狀態的進程,使之轉爲就緒狀態,以繼續運行。死鎖的檢測和解除措施,有可能使系統得到較好的資源利用率和吞吐量,但在實 現上難度也最大。併發

Java中

/** 線程Thread1率先佔有了resource1, 繼續運行時須要resource2, 但此時resource2卻被線程Thread2佔有了, 所以只能等待Thread2釋放resource2纔可以繼續運行; 同時,Thread2也須要resource1, 它只能等待Thread1釋放resource1纔可以繼續運行, 所以,Thread1和Thread2都處於等待狀態, 誰也沒法繼續運行,即產生了死鎖。 **/
public class DeadLock { 
 
  

    public static void main(String[] args) { 
 
  
        dead_lock();
    }

    private static void dead_lock() { 
 
  
        // 兩個資源
        final Object resource1 = "resource1";
        final Object resource2 = "resource2";
        
        // 第一個線程,想先佔有resource1,再嘗試着佔有resource2
        Thread t1 = new Thread() { 
 
  
            public void run() { 
 
  
                // 嘗試佔有resource1
                synchronized (resource1) { 
 
  
                    // 成功佔有resource1
                    System.out.println("Thread1 1:locked resource1");
                    // 休眠一段時間
                    try { 
 
  
                        Thread.sleep(50);
                    } catch (InterruptedException e) { 
 
  
                        e.printStackTrace();
                    }

                    // 嘗試佔有resource2,若是不能佔有,該線程會一直等到
                    synchronized (resource2) { 
 
  
                        System.out.println("Thread1 1:locked resource2");
                    }
                } //syn
            } //run
        }; //Thread t1
    
        // 第二個線程,想先佔有resource2,再佔有resource1
        Thread t2 = new Thread() { 
 
  
            public void run() { 
 
  
                // 嘗試佔有resource2
                synchronized (resource2) { 
 
  
                    // 成功佔有resource2
                    System.out.println("Thread 2 :locked resource2");
                    // 休眠一段時間
                    try { 
 
  
                        Thread.sleep(50);
                    } catch (InterruptedException e) { 
 
  
                        e.printStackTrace();
                    }
                
                    // 嘗試佔有resource1,若是不能佔有,該線程會一直等到
                    synchronized (resource1) { 
 
  
                        System.out.println("Thread1 2:locked resource1");
                    }
                } //syn
            } //run
        }; //Thread t2
    
    // 啓動線程
    t1.start();
    t2.start();
    }
}

死鎖的另外一種:遞歸死鎖,舉例:
所謂遞歸函數就是自調用函數,在函數體內直接或間接的調用本身,即函數的嵌套是函數自己。
遞歸方式有兩種:直接遞歸和間接遞歸,直接遞歸就是在函數中出現調用函數自己。間接遞歸,指函數中調用了其餘函數,而該其餘函數又調用了本函數。
那何時使用遞歸呢?通常來講當你要在某段代碼邏輯中使用循環迭代的時候可是迭代的次數在迭代以前沒法知曉的狀況下使用遞歸。打個比方你要在一個文件夾中查找某個文件,而這個文件夾底下有N多子文件夾和文件,當你在不知道有多少層文件夾和文件的狀況下你就得用到遞歸了。
遞歸的優勢就是讓代碼顯得很簡潔,同時有些應用場景不得不使用遞歸好比前面說的找文件。遞歸是個好東西可是在某些時候也會給你帶來一些麻煩。好比在多線程的環境下使用遞歸,遇到了多線程那麼就不得不面對同步的問題。而遞歸程序遇到同步的時候很容易出問題。
多線程的遞歸就是指遞歸鏈中的某個方法由另一個線程來操做,如下代碼的都是意爲調用recursive()和businessLogic(),並不是一個線程。

[java] view plain copy
 
public class Test { 
 
    
    public void recursive(){ 
 
    
        this.businessLogic();  
    }  
    public synchronized void businessLogic(){ 
 
    
        System.out.println("處理業務邏輯");  
    System.out.println("<a href="http://lib.csdn.net/base/mysql" class='replace_word' title="MySQL" target='_blank' style='color:#df3434; font-weight:bold;'></a>");  
        this.recursive();  
    }  
}

以上這段代碼就是個能造成死鎖的代碼,事實上這個「synchronized」放在「businessLogic()」和「recursive()」都會造成死鎖,而且是多線程的狀況下就會鎖住!他的邏輯順序是先執行recursive()方法而後接下來執行businessLogic()方法同時將businessLogic()方法鎖住,接下來程序進入businessLogic()方法內部執行完打印語句後開始執行recursive(),進入recursive()後準備執行businessLogic(),問題就出現了,以前執行的businessLogic()的鎖尚未放開此次又執行到這裏了,固然是過不去的了,造成了死鎖!
從這個例子咱們總結出來一個規律就是在遞歸的時候在遞歸鏈上面的方法上加鎖確定會出現死鎖(所謂遞歸鏈就是指recursive()鏈向businessLogic(),businessLogic()又鏈回recursive()),解決這個問題的方法就是避免在遞歸鏈上加鎖,請看如下的例子:

[java] view plain copy
 
public class Test { 
 
    
    public void recursive(){ 
 
    
        this.businessLogic();  
    }  
    public  void businessLogic(){ 
 
    
        System.out.println("處理業務邏輯");  
        this.saveToDB();  
        this.recursive();  
    }  
    public synchronized void saveToDB(){ 
 
    
        System.out.println("保存到數據庫");  
    }  
}

saveToDB()不在這條遞歸鏈上面天然不會出現死鎖,因此說在遞歸中加鎖是件很危險的事情,實在逃不過要加鎖就加在最小的粒度的程序代碼上以減少死鎖的機率。

避免死鎖

死鎖在有些狀況下死鎖是能夠避免的。通常有三種用於避免死鎖的技術:

  • 加鎖順序
  • 加鎖時限
  • 死鎖檢測

1.加鎖順序

當多個線程須要相同的一些鎖,可是按照不一樣的順序加鎖,死鎖就很容易發生。
若是能確保全部的線程都是按照相同的順序得到鎖,那麼死鎖就不會發生。看下面這個例子:

Thread 1:
  lock A 
  lock B

Thread 2:
   wait for A
   lock C (when A locked)

Thread 3:
   wait for A
   wait for B
   wait for C

若是一個線程(好比線程3)須要一些鎖,那麼它必須按照肯定的順序獲取鎖。它只有得到了從順序上排在前面的鎖以後,才能獲取後面的鎖。

例如,線程2和線程3只有在獲取了鎖A以後才能嘗試獲取鎖C(譯者注:獲取鎖A是獲取鎖C的必要條件)。由於線程1已經擁有了鎖A,因此線程2和3須要一直等到鎖A被釋放。而後在它們嘗試對B或C加鎖以前,必須成功地對A加了鎖。

按照順序加鎖是一種有效的死鎖預防機制。可是,這種方式須要你事先知道全部可能會用到的鎖及順序,但總有些時候是沒法預知的。

2.加鎖時限

另一個能夠避免死鎖的方法是在嘗試獲取鎖的時候加一個超時時間,這也就意味着在嘗試獲取鎖的過程當中若超過了這個時限該線程則放棄對該鎖請求。若一個線程沒有在給定的時限內成功得到全部須要的鎖,則會進行回退並釋放全部已經得到的鎖,而後等待一段隨機的時間再重試。這段隨機的等待時間讓其它線程有機會嘗試獲取相同的這些鎖,而且讓該應用在沒有得到鎖的時候能夠繼續運行(譯者注:加鎖超時後能夠先繼續運行乾點其它事情,再回頭來重複以前加鎖的邏輯)。

如下是一個例子,展現了兩個線程以不一樣的順序嘗試獲取相同的兩個鎖,在發生超時後回退並重試的場景

Thread 1 locks A
Thread 2 locks B

Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked

Thread 1's lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.

Thread 2's lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.

在上面的例子中,線程2比線程1早200毫秒進行重試加鎖,所以它能夠先成功地獲取到兩個鎖。這時,線程1嘗試獲取鎖A而且處於等待狀態。當線程2結束時,線程1也能夠順利的得到這兩個鎖(除非線程2或者其它線程在線程1成功得到兩個鎖以前又得到其中的一些鎖)。

須要注意的是,因爲存在鎖的超時,因此咱們不能認爲這種場景就必定是出現了死鎖。也多是由於得到了鎖的線程(致使其它線程超時)須要很長的時間去完成它的任務。

此外,若是有很是多的線程同一時間去競爭同一批資源,就算有超時和回退機制,仍是可能會致使這些線程重複地嘗試但卻始終得不到鎖。若是隻有兩個線程,而且重試的超時時間設定爲0到500毫秒之間,這種現象可能不會發生,可是若是是10個或20個線程狀況就不一樣了。由於這些線程等待相等的重試時間的機率就高的多(或者很是接近以致於會出現問題)。
(譯者注:超時和重試機制是爲了不在同一時間出現的競爭,可是當線程不少時,其中兩個或多個線程的超時時間同樣或者接近的可能性就會很大,所以就算出現競爭而致使超時後,因爲超時時間同樣,它們又會同時開始重試,致使新一輪的競爭,帶來了新的問題。)

這種機制存在一個問題,在Java中不能對synchronized同步塊設置超時時間。你須要建立一個自定義鎖,或使用Java5中java.util.concurrent包下的工具。寫一個自定義鎖類不復雜,但超出了本文的內容。後續的Java併發系列會涵蓋自定義鎖的內容。

3.死鎖檢測

死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖而且鎖超時也不可行的場景。

每當一個線程得到了鎖,會在線程和鎖相關的數據結構中(map、graph等等)將其記下。除此以外,每當有線程請求鎖,也須要記錄在這個數據結構中。

當一個線程請求鎖失敗時,這個線程能夠遍歷鎖的關係圖看看是否有死鎖發生。例如,線程A請求鎖7,可是鎖7這個時候被線程B持有,這時線程A就能夠檢查一下線程B是否已經請求了線程A當前所持有的鎖。若是線程B確實有這樣的請求,那麼就是發生了死鎖(線程A擁有鎖1,請求鎖7;線程B擁有鎖7,請求鎖1)。

固然,死鎖通常要比兩個線程互相持有對方的鎖這種狀況要複雜的多。線程A等待線程B,線程B等待線程C,線程C等待線程D,線程D又在等待線程A。線程A爲了檢測死鎖,它須要遞進地檢測全部被B請求的鎖。從線程B所請求的鎖開始,線程A找到了線程C,而後又找到了線程D,發現線程D請求的鎖被線程A本身持有着。這是它就知道發生了死鎖。

下面是一幅關於四個線程(A, B, C和D)之間鎖佔有和請求的關係圖。像這樣的數據結構就能夠被用來檢測死鎖。
在這裏插入圖片描述
那麼當檢測出死鎖時,這些線程該作些什麼呢?

一個可行的作法是釋放全部鎖,回退,而且等待一段隨機的時間後重試。這個和簡單的加鎖超時相似,不同的是隻有死鎖已經發生了纔回退,而不會是由於加鎖的請求超時了。雖然有回退和等待,可是若是有大量的線程競爭同一批鎖,它們仍是會重複地死鎖(編者注:緣由同超時相似,不能從根本上減輕競爭)。

一個更好的方案是給這些線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖同樣繼續保持着它們須要的鎖。若是賦予這些線程的優先級是固定不變的,同一批線程老是會擁有更高的優先級。爲避免這個問題,能夠在死鎖發生的時候設置隨機的優先級。

銀行家算法

咱們能夠把操做系統看做是銀行家,操做系統管理的資源至關於銀行家管理的資金,進程向操做系統請求分配資源至關於用戶向銀行家貸款。
爲保證資金的安全,銀行家規定:
(1) 當一個顧客對資金的最大需求量不超過銀行家現有的資金時就可接納該顧客;
(2) 顧客能夠分期貸款,但貸款的總數不能超過最大需求量;
(3) 當銀行家現有的資金不能知足顧客尚需的貸款數額時,對顧客的貸款可推遲支付,但總能使顧客在有限的時間裏獲得貸款;
(4) 當顧客獲得所需的所有資金後,必定能在有限的時間裏歸還全部的資金.

操做系統按照銀行家制定的規則爲進程分配資源,當進程首次申請資源時,要測試該進程對資源的最大需求量,若是系統現存的資源能夠知足它的最大需求量則按當前的申請量分配資源,不然就推遲分配。當進程在執行中繼續申請資源時,先測試該進程本次申請的資源數是否超過了該資源所剩餘的總量。若超過則拒絕分配資源,若能存在安全狀態,則按當前的申請量分配資源,不然也要推遲分配。