一、概述
死鎖是指兩個或兩個以上的進程在執行過程中,因爭搶資源而造成的一種互相等待的現象,若無外力干涉它們將無法推進,如果系統資源充足,進程的資源請求能夠得到滿足,死鎖出現的可能性就很低,否則就會因爭奪有限的資源而陷入死鎖。
死鎖產生的原因:【1】系統資源不足;【2】資源分配不當;【3】進程運行推進的順序不合適;
形成死鎖的四個必要條件:
【1】互斥條件:一個資源每次只能被一個進程使用。
【2】請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
【3】不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
【4】循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系。
二、代碼演示
三、排查死鎖
【1】jps 命令定位進程號:window 下 java 運行程序,也有類似與 Linux 操作系統的 ps -ef|grep xxx 的查看進程的命令,我們這里只查看 java 的進程,即使用 jps 命令
【2】jstack 能夠找到死鎖信息:
四、如何避免線程死鎖
【1】破壞互斥條件:這個條件我們沒有辦法破壞,因為我們用鎖本來就是想讓他們互斥的(臨界資源需要互斥訪問)。
【2】破壞請求與保持條件:一次性申請所有的資源。
【3】破壞不剝奪條件:占用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它占有的資源。
【4】破壞循環等待條件:靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。破壞循環等待條件。
避免死鎖可以概括成三種方法:
【1】固定加鎖的順序(針對鎖順序死鎖);
【2】開放調用(針對對象之間協作造成的死鎖);
【3】使用定時鎖 tryLock();使用顯式 Lock鎖,在獲取鎖時使用 tryLock()
方法。當等待超過時限的時候,tryLock()
不會一直等待,而是返回錯誤信息。使用tryLock()
能夠有效避免死鎖問題。
如果等待獲取鎖時間超時,則拋出異常而不是一直等待!
五、死鎖案例及解決方案
【1】不同的加鎖順序案例:
【2】 上面的例子中,我們模擬一個轉賬的過程,amount用來表示用戶余額。transfer用來將當前賬號的一部分金額轉移到目標對象中。為了保證在 transfer的過程中,兩個賬戶不被別人修改,我們使用了兩個synchronized 關鍵字,分別把 transfer對象和目標對象進行鎖定。看起來好像沒問題,但是我們沒有考慮在調用的過程中,transfer的順序是可以發送變化的:
1 DiffLockOrder account1 = new DiffLockOrder(1000); 2 DiffLockOrder account2 = new DiffLockOrder(500); 3 4 Runnable target1= ()->account1.transfer(account2,200); 5 Runnable target2= ()->account2.transfer(account1,100); 6 new Thread(target1).start(); 7 new Thread(target2).start();
上面的例子中,我們定義了兩個account,然后兩個賬戶互相轉賬,最后很有可能導致互相鎖定,最后產生死鎖。
解決方案一:使用 private類變量,只是用一個 sync就可以在所有的實例中同步,來解決兩個 sync順序問題。因為類變量是在所有實例中共享的,這樣一次sync就夠了:
1 public class LockWithPrivateStatic { 2 3 private int amount; 4 // 不管有多少個實例,共享同一個 lock 5 private static final Object lock = new Object(); 6 7 public LockWithPrivateStatic(int amount){ 8 this.amount=amount; 9 } 10 11 public void transfer(LockWithPrivateStatic target, int transferAmount){ 12 synchronized (lock) { 13 if (amount < transferAmount) { 14 System.out.println("余額不足!"); 15 } else { 16 amount = amount - transferAmount; 17 target.amount = target.amount + transferAmount; 18 } 19 } 20 } 21 }
解決方案二:使用相同的Order,我們產生死鎖的原因是無法控制上鎖的順序,如果我們能夠控制上鎖的順序,是不是就不會產生死鎖了呢?帶着這個思路,我們給對象再加上一個 id字段:
1 private final long id; // 唯一ID,用來排序 2 private static final AtomicLong nextID = new AtomicLong(0); // 用來生成ID 3 4 public DiffLockWithOrder(int amount){ 5 this.amount=amount; 6 this.id = nextID.getAndIncrement(); 7 }
在初始化對象的時候,我們使用 static的 AtomicLong類來為每個對象生成唯一的ID。在做 transfer的時候,我們先比較兩個對象的ID大小,然后根據 ID進行排序,最后安裝順序進行加鎖。這樣就能夠保證順序,從而避免死鎖。
1 public void transfer(DiffLockWithOrder target, int transferAmount){ 2 //將加鎖的對象修改為可變參數,ID小的永遠為第一個鎖對象 3 DiffLockWithOrder fist, second; 4 5 if (compareTo(target) < 0) { 6 fist = this; 7 second = target; 8 } else { 9 fist = target; 10 second = this; 11 } 12 13 synchronized (fist){ 14 synchronized (second){ 15 if(amount< transferAmount){ 16 System.out.println("余額不足!"); 17 }else{ 18 amount=amount-transferAmount; 19 target.amount=target.amount+transferAmount; 20 } 21 } 22 } 23 }
解決方案三:釋放掉已占有的鎖,死鎖是互相請求對方占用的鎖,但是對方的鎖一直沒有釋放,我們考慮一下,如果獲取不到鎖的時候,自動釋放已占用的鎖是不是也可以解決死鎖的問題呢?因為 ReentrantLock有一個 tryLock()方法,我們可以使用這個方法來判斷是否能夠獲取到鎖,獲取不到就釋放已占有的鎖。我們使用 ReentrantLock來完成這個例子:
1 public class DiffLockWithReentrantLock { 2 3 private int amount; 4 private final Lock lock = new ReentrantLock(); 5 6 public DiffLockWithReentrantLock(int amount){ 7 this.amount=amount; 8 } 9 10 private void transfer(DiffLockWithReentrantLock target, int transferAmount) 11 throws InterruptedException { 12 while (true) { 13 if (this.lock.tryLock()) { 14 try { 15 if (target.lock.tryLock()) { 16 try { 17 if(amount< transferAmount){ 18 System.out.println("余額不足!"); 19 }else{ 20 amount=amount-transferAmount; 21 target.amount=target.amount+transferAmount; 22 } 23 break; 24 } finally { 25 target.lock.unlock(); 26 } 27 } 28 } finally { 29 this.lock.unlock(); 30 } 31 } 32 //隨機sleep一定的時間,保證可以釋放掉鎖 33 Thread.sleep(1000+new Random(1000L).nextInt(1000)); 34 } 35 } 36 37 }
我們把兩個 tryLock方法在 while循環中,如果不能獲取到鎖就循環遍歷。