1、死鎖的定義java
多線程以及多進程改善了系統資源的利用率並提升了系統 的處理能力。然而,併發執行也帶來了新的問題——死鎖。所謂死鎖是指多個線程因競爭資源而形成的一種僵局(互相等待),若無外力做用,這些進程都將沒法向前推動。數據結構
所謂死鎖是指兩個或兩個以上的線程在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。多線程
下面咱們經過一些實例來講明死鎖現象。併發
先看生活中的一個實例,兩我的面對面過獨木橋,甲和乙都已經在橋上走了一段距離,即佔用了橋的資源,甲若是想經過獨木橋的話,乙必須退出橋面讓出橋的資源,讓甲經過,可是乙不服,爲何讓我先退出去,我還想先過去呢,因而就僵持不下,致使誰也過不了橋,這就是死鎖。dom
在計算機系統中也存在相似的狀況。例如,某計算機系統中只有一臺打印機和一臺輸入 設備,進程P1正佔用輸入設備,同時又提出使用打印機的請求,但此時打印機正被進程P2 所佔用,而P2在未釋放打印機以前,又提出請求使用正被P1佔用着的輸入設備。這樣兩個進程相互無休止地等待下去,均沒法繼續執行,此時兩個進程陷入死鎖狀態。ide
2、死鎖產生的緣由工具
一、系統資源的競爭ui
一般系統中擁有的不可剝奪資源,其數量不足以知足多個進程運行的須要,使得進程在運行過程當中,會因爭奪資源而陷入僵局,如磁帶機、打印機等。只有對不可剝奪資源的競爭纔可能產生死鎖,對可剝奪資源的競爭是不會引發死鎖的。this
二、進程推動順序非法spa
進程在運行過程當中,請求和釋放資源的順序不當,也一樣會致使死鎖。例如,併發進程 P一、P2分別保持了資源R一、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,這個是產生死鎖的最根本緣由。另外一個緣由是默認的鎖申請操做是阻塞的。
三、死鎖產生的必要條件:
產生死鎖必須同時知足如下四個條件,只要其中任一條件不成立,死鎖就不會發生。
(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)循環等待條件:若干進程之間造成一種頭尾相接的循環等待資源關係。若是乙不退出橋面,甲不能經過,甲不退出橋面,乙不能經過。
3、死鎖實例
例子1:
package com.demo.test; /** * 一個簡單的死鎖類 * t1先運行,這個時候flag==true,先鎖定obj1,而後睡眠1秒鐘 * 而t1在睡眠的時候,另外一個線程t2啓動,flag==false,先鎖定obj2,而後也睡眠1秒鐘 * t1睡眠結束後須要鎖定obj2才能繼續執行,而此時obj2已被t2鎖定 * t2睡眠結束後須要鎖定obj1才能繼續執行,而此時obj1已被t1鎖定 * t一、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以完成自身的操做,兩個線程都想獲得對方的資源,而不肯釋放本身的資源,形成兩個線程都在等待,而沒法執行的狀況。
例子2:
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,從而致使了死鎖。在線程引發死鎖的過程當中,就造成了一個依賴於資源的循環。
4、如何避免死鎖
在有些狀況下死鎖是能夠避免的。下面介紹三種用於避免死鎖的技術:
一、加鎖順序
當多個線程須要相同的一些鎖,可是按照不一樣的順序加鎖,死鎖就很容易發生。若是能確保全部的線程都是按照相同的順序得到鎖,那麼死鎖就不會發生。看下面這個例子:
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加了鎖。
按照順序加鎖是一種有效的死鎖預防機制。可是,這種方式須要你事先知道全部可能會用到的鎖(並對這些鎖作適當的排序),但總有些時候是沒法預知的。
下面對例子1進行改造:
將
Thread t2 = new Thread(new DeadLock(false), "線程2");
改成:
Thread t2 = new Thread(new DeadLock(true), "線程2");
如今應該不會出現死鎖了,由於線程1和線程2都是先對obj1加鎖,而後再對obj2加鎖,當t1啓動後,鎖住了obj1,而t2也啓動後,只有當t1釋放了obj1後t2纔會執行,從而有效的避免了死鎖。
運行結果:
線程1運行
線程1已經鎖住obj1
線程2運行
1秒鐘後,線程1鎖住obj2
線程2已經鎖住obj1
1秒鐘後,線程2鎖住obj2
例子2改造:
package com.demo.test; public class SyncThread1 implements Runnable{ private Object obj1; private Object obj2; public SyncThread1(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(); } System.out.println(name + " released lock on "+obj1); synchronized(obj2){ System.out.println("After, "+ name + " acquired lock on "+obj2); work(); } System.out.println(name + " released lock on "+obj2); System.out.println(name + " finished execution."); } private void work() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }
package com.demo.test; public class ThreadDeadTest1 { 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 SyncThread1(obj1, obj2), "t1"); Thread t2 = new Thread(new SyncThread1(obj2, obj3), "t2"); Thread t3 = new Thread(new SyncThread1(obj3, obj1), "t3"); t1.start(); Thread.sleep(1000); t2.start(); Thread.sleep(1000); t3.start(); } }
運行結果:
t1 acquired lock on java.lang.Object@60e128
t2 acquired lock on java.lang.Object@18b3364
t3 acquired lock on java.lang.Object@76fba0
t1 released lock on java.lang.Object@60e128
t2 released lock on java.lang.Object@18b3364
After, t1 acquired lock on java.lang.Object@18b3364
t3 released lock on java.lang.Object@76fba0
After, t2 acquired lock on java.lang.Object@76fba0
After, t3 acquired lock on java.lang.Object@60e128
t1 released lock on java.lang.Object@18b3364
t1 finished execution.
t2 released lock on java.lang.Object@76fba0
t3 released lock on java.lang.Object@60e128
t3 finished execution.
t2 finished execution.
從結果中看,沒有出現死鎖的局面。由於在run()方法中,不存在嵌套封鎖。
避免嵌套封鎖:這是死鎖最主要的緣由的,若是你已經有一個資源了就要避免封鎖另外一個資源。若是你運行時只有一個對象封鎖,那是幾乎不可能出現一個死鎖局面的。
再舉個生活中的例子,好比銀行轉帳的場景下,咱們必須同時得到兩個帳戶上的鎖,才能進行操做,兩個鎖的申請必須發生交叉。這時咱們也能夠打破死鎖的那個閉環,在涉及到要同時申請兩個鎖的方法中,老是以相同的順序來申請鎖,好比老是先申請 id 大的帳戶上的鎖 ,而後再申請 id 小的帳戶上的鎖,這樣就沒法造成致使死鎖的那個閉環。
public class Account { private int id; // 主鍵 private String name; private double balance; public void transfer(Account from, Account to, double money){ if(from.getId() > to.getId()){ synchronized(from){ synchronized(to){ // transfer } } }else{ synchronized(to){ synchronized(from){ // transfer } } } } public int getId() { return id; } }
這樣的話,即便發生了兩個帳戶好比 id=1的和id=100的兩個帳戶相互轉帳,由於無論是哪一個線程先得到了id=100上的鎖,另一個線程都不會去得到id=1上的鎖(由於他沒有得到id=100上的鎖),只能是哪一個線程先得到id=100上的鎖,哪一個線程就先進行轉帳。這裏除了使用id以外,若是沒有相似id這樣的屬性能夠比較,那麼也可使用對象的hashCode()的值來進行比較。
二、加鎖時限
另一個能夠避免死鎖的方法是在嘗試獲取鎖的時候加一個超時時間,這也就意味着在嘗試獲取鎖的過程當中若超過了這個時限該線程則放棄對該鎖請求。若一個線程沒有在給定的時限內成功得到全部須要的鎖,則會進行回退並釋放全部已經得到的鎖,而後等待一段隨機的時間再重試。這段隨機的等待時間讓其它線程有機會嘗試獲取相同的這些鎖,而且讓該應用在沒有得到鎖的時候能夠繼續運行(加鎖超時後能夠先繼續運行乾點其它事情,再回頭來重複以前加鎖的邏輯)。
如下是一個例子,展現了兩個線程以不一樣的順序嘗試獲取相同的兩個鎖,在發生超時後回退並重試的場景:
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包下的工具。
三、死鎖檢測
死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖而且鎖超時也不可行的場景。
每當一個線程得到了鎖,會在線程和鎖相關的數據結構中(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)之間鎖佔有和請求的關係圖。像這樣的數據結構就能夠被用來檢測死鎖。
那麼當檢測出死鎖時,這些線程該作些什麼呢?
一個可行的作法是釋放全部鎖,回退,而且等待一段隨機的時間後重試。這個和簡單的加鎖超時相似,不同的是隻有死鎖已經發生了纔回退,而不會是由於加鎖的請求超時了。雖然有回退和等待,可是若是有大量的線程競爭同一批鎖,它們仍是會重複地死鎖(緣由同超時相似,不能從根本上減輕競爭)。
一個更好的方案是給這些線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖同樣繼續保持着它們須要的鎖。若是賦予這些線程的優先級是固定不變的,同一批線程老是會擁有更高的優先級。爲避免這個問題,能夠在死鎖發生的時候設置隨機的優先級。
總結:避免死鎖的方式
一、讓程序每次至多隻能得到一個鎖。固然,在多線程環境下,這種狀況一般並不現實。
二、設計時考慮清楚鎖的順序,儘可能減小嵌在的加鎖交互數量。
三、既然死鎖的產生是兩個線程無限等待對方持有的鎖,那麼只要等待時間有個上限不就行了。固然synchronized不具有這個功能,可是咱們可使用Lock類中的tryLock方法去嘗試獲取鎖,這個方法能夠指定一個超時時限,在等待超過該時限以後便會返回一個失敗信息。
咱們可使用ReentrantLock.tryLock()方法,在一個循環中,若是tryLock()返回失敗,那麼就釋放以及得到的鎖,並睡眠一小段時間。這樣就打破了死鎖的閉環。好比:線程T1持有鎖L1而且申請得到鎖L2,而線程T2持有鎖L2而且申請得到鎖L3,而線程T3持有鎖L3而且申請得到鎖L1。此時若是T3申請鎖L1失敗,那麼T3釋放鎖L3,並進行睡眠,那麼T2就能夠得到L3了,而後T2執行完以後釋放L2, L3,因此T1也能夠得到L2了執行完而後釋放鎖L1, L2,而後T3睡眠醒來,也能夠得到L1, L3了。打破了死鎖的閉環。