Java程序死鎖問題定位與解決


 

一、概述


死鎖是指兩個或兩個以上的進程在執行過程中,因爭搶資源而造成的一種互相等待的現象,若無外力干涉它們將無法推進,如果系統資源充足,進程的資源請求能夠得到滿足,死鎖出現的可能性就很低,否則就會因爭奪有限的資源而陷入死鎖。

死鎖產生的原因:【1】系統資源不足;【2】資源分配不當;【3】進程運行推進的順序不合適

形成死鎖的四個必要條件:
【1】互斥條件:一個資源每次只能被一個進程使用。
【2】請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放
【3】不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
【4】循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系。

二、代碼演示


 1 public class TestMian {
 2         //A、B 表示兩把鎖
 3         String A = "A";
 4         String B = "B";
 5     public static void main(String[] args) {
 6         TestMian testMian = new TestMian();
 7         new Thread(()->{
 8             try {
 9                 testMian.a();
10             } catch (InterruptedException e) {
11                 e.printStackTrace();
12             }
13         }).start();
14 
15         new Thread(()->{
16             try {
17                 testMian.b();
18             } catch (InterruptedException e) {
19                 e.printStackTrace();
20             }
21         }).start();
22     }
23 
24 
25     public void a() throws InterruptedException {
26         //持有鎖A后,嘗試持有鎖B   ***********重點**************
27         synchronized (A){
28             System.out.println("A");
29             TimeUnit.SECONDS.sleep(1);
30             synchronized (B){
31                 System.out.println("B");
32             }
33         }
34     }
35 
36     public void b() throws InterruptedException {
37         //持有鎖B后,嘗試持有鎖A   ***********重點**************
38         synchronized (B){
39             System.out.println("B");
40             TimeUnit.SECONDS.sleep(1);
41             synchronized (A){
42                 System.out.println("A");
43             }
44         }
45     }
46 }

三、排查死鎖


【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】不同的加鎖順序案例:

 1 public class DiffLockOrder {
 2  
 3     private int amount;
 4  
 5     public DiffLockOrder(int amount){
 6        this.amount=amount;
 7     }
 8  
 9     // 第一個鎖 this 是 DiffLockOrder的對象,第二個鎖target 也是 DiffLockOrder對象,為死鎖埋下了隱患
10     public void transfer(DiffLockOrder target,int transferAmount){
11         synchronized (this){
12             synchronized (target){
13                 if(amount< transferAmount){
14                     System.out.println("余額不足!");
15                 }else{
16                     amount=amount-transferAmount;
17                     target.amount=target.amount+transferAmount;
18                 }
19             }
20         }
21     }
22 }

【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循環中,如果不能獲取到鎖就循環遍歷。 


免責聲明!

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



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