JAVA中死鎖問題排查和預(yù)防

在Java多線程開發(fā)中死鎖問題并不少見,當(dāng)線程間相互等待資源,而又不釋放自身的資源時就會導(dǎo)致無窮無盡的等待。

舉一個死鎖的例子

public class Account {

    private int balance;
    // 轉(zhuǎn)賬
    void transfer(Account target, int amt) {
        // 鎖定轉(zhuǎn)出賬戶
        synchronized(this) {
            try {
                Thread.sleep(10);
            }catch (Exception e){
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"lock:"+this+"=>get:"+target);
            // 鎖定轉(zhuǎn)入賬戶
            synchronized(target) {
                if (this.balance > amt) {
                    this.balance -= amt;
                    target.balance += amt;
                }
                System.out.println(Thread.currentThread().getName()+"lock:"+target+"=>get:"+this);

            }
        }
    }


    public static void main(String[] args){
         Account account1 = new Account();
         Account account2 = new Account();
        new Thread(new Runnable() {
            public void run() {
                account1.transfer(account2,10);
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                account2.transfer(account1,10);
            }
        }).start();


    }
}

以上是一個轉(zhuǎn)賬的例子,兩個賬戶相互轉(zhuǎn)賬,轉(zhuǎn)賬時必須要保護自己賬戶的資源balance和目標(biāo)的資源balance不會被其他線程修改,就做了加鎖。在單線程的情況下時不會有問題的,但是一旦有兩個線程同時操作兩個賬戶轉(zhuǎn)賬就會出現(xiàn)死鎖的問題。兩個線程都在等對方先釋放資源,會永久地等下去。

如何解決死鎖地問題

并發(fā)程序出現(xiàn)死鎖問題并沒什么好地解決辦法,一般情況下只能重啟應(yīng)用。因此解決死鎖地問題最好的辦法就是規(guī)避死鎖。如何規(guī)避呢?有一個叫Coffman的牛人總結(jié)出來,只有以下四個條件都發(fā)生的時候才會出現(xiàn)死鎖。

  1. 互斥,共享資源 X 和 Y 只能被一個線程占用;
  2. 占有且等待,線程 T1 已經(jīng)取得共享資源 X,在等待共享資源 Y 的時候,不釋放共享資源 X;
  3. 不可搶占,其他線程不能強行搶占線程 T1 占有的資源;
  4. 循環(huán)等待,線程 T1 等待線程 T2 占有的資源,線程 T2 等待線程 T1 占有的資源,就是循環(huán)等待。

那么也就是說這四個條件只要破壞其中一個就不會發(fā)生死鎖。首先第一個條件互斥是無法被破壞的,因為我們在多線程環(huán)境里加鎖就是為了互斥。其他三個條件都是可以被破壞的。

破壞占有且等待條件

要破壞這個條件通常的做法是讓一個線程一次性申請所有的資源,在上面的例子上我們可以再建一個單例類Allocator,Allocator來一次性申請兩個賬戶的資源,轉(zhuǎn)賬完成后就一起釋放資源。

public class Allocator {
    private Allocator(){};
    private static Allocator instance = new Allocator();
    private List<Object> als = new ArrayList<>();
    // 一次性申請所有資源
    synchronized boolean apply(Object from, Object to){
        if(als.contains(from) || als.contains(to)){
            return false;
        } else {
            als.add(from);
            als.add(to);
        }
        return true;
    }
    // 歸還資源
    synchronized void free(Object from, Object to){
        als.remove(from);
        als.remove(to);
    }
    public static Allocator getInstance(){
        return instance;
    }
}

class Account {
    // actr 應(yīng)該為單例
    private Allocator actr = Allocator.getInstance();
    private int balance;
    // 轉(zhuǎn)賬
    void transfer(Account target, int amt){
        // 一次性申請轉(zhuǎn)出賬戶和轉(zhuǎn)入賬戶,直到成功
        while(!actr.apply(this, target))
        try{
            // 鎖定轉(zhuǎn)出賬戶
            synchronized(this){
                // 鎖定轉(zhuǎn)入賬戶
                synchronized(target){
                    if (this.balance > amt){
                        this.balance -= amt;
                        target.balance += amt;
                    }
                }
            }
        } finally {
            actr.free(this, target);
        }
    }
    public static void main(String[] args) {
        Account account1 = new Account();
        Account account2 = new Account();
        new Thread(new Runnable() {
            public void run() {
                account1.transfer(account2,10);
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                account2.transfer(account1,10);
            }
        }).start();
    }
}

破壞不可搶占條件

破壞不可強制資源其實就是線程能夠主動釋放它占有的資源,這一點 synchronized是做不到的。原因是 synchronized 申請資源的時候,如果申請不到,線程直接進入阻塞狀態(tài)了,而線程進入阻塞狀態(tài),啥都干不了,也釋放不了線程已經(jīng)占有的資源。但是可以使用SDK下的java.util.concurrent 這個包下面提供的 Lock類來解決這個問題。

public class Account {

    private final Lock lock = new ReentrantLock();
    private int balance;
    // 轉(zhuǎn)賬
    void transfer(Account target, int amt) throws InterruptedException {
            while(true){
              if(this.lock.tryLock()){ 
                 try {
                     if(target.lock.tryLock()){ 
                         try {
                             if (this.balance > amt) {
                                 this.balance -= amt;
                                 target.balance += amt;
                             }
                         }finally {
                             target.lock.unlock();
                         }

                     }
                 }finally {
                     this.lock.unlock();
                 }
            }
           }
    }
    public static void main(String[] args){
         Account account1 = new Account();
         Account account2 = new Account();
        new Thread(new Runnable() {
            public void run() {
                try {
                    account1.transfer(account2,10);
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                try {
                    account2.transfer(account1,10);
                }catch (Exception e){
                    e.printStackTrace();
                }
           
            }
        }).start();
    }
}

破壞循環(huán)等待條件

破壞這個條件,需要對資源進行排序,然后按序申請資源。以上賬號的例子我們假設(shè)每一個賬戶都有一個id字段,我們用id字段作為排序條件。申請的時候,我們可以按照從小到大的順序來申請,這樣無論有多少個線程進來都會先去拿賬戶id小的賬戶資源,這樣就不會出現(xiàn)爭搶的問題。

public class Account {
    private int id;
    private int balance;

    public Account(int id) {
        this.id = id;
    }

    // 轉(zhuǎn)賬
    void transfer(Account target, int amt){
        Account left = this;
        Account right = target;
        if (this.id > target.id) {
            left = target;
            right = this;
        }
        // 鎖定序號小的賬戶
        synchronized(left){
            // 鎖定序號大的賬戶
            synchronized(right){
                if (this.balance > amt){
                    this.balance -= amt;
                    target.balance += amt;
                }
            }
        }
    }


    public static void main(String[] args){
        Account account1 = new Account(1);
        Account account2 = new Account(2);
        new Thread(new Runnable() {
            public void run() {
                account1.transfer(account2,10);
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                account2.transfer(account1,10);
            }
        }).start();
    }
}

有了以上這三個方案后你可能會有一個疑問,那個方案好呢。從性能上看破壞占有且等待條件這個方案性能最低,它需要同時獲得兩把鎖的使用權(quán)才能執(zhí)行下去,不然就會一直while下去。破壞不可搶占條件這個性能次之,它不用同時獲得兩個資源的鎖,一個資源一個資源的拿,一旦拿不到就會主動釋放,但它也會一直while的嘗試下去。破壞循環(huán)等待條件這個性能最佳,它會提前把資源順序排序好,避免了發(fā)生掙搶的問題。但是破壞循環(huán)等待條件的做法對代碼產(chǎn)生侵入性,增加了額外的邏輯。所以最優(yōu)的要看具體的業(yè)務(wù)性能要求,通常的話破壞不可搶占條件這個方案是一個常用的選擇。

線上如何排查死鎖的問題

線上死鎖問題總是不經(jīng)意間產(chǎn)生的,跑在tomcat上的應(yīng)用一旦出現(xiàn)死鎖問題就會照成大部分線程阻塞,進而tomcat就會出現(xiàn)假死狀態(tài)不能正常的服務(wù)。所以排查死鎖問題是java程序員必備的一個技能。
我們可以使用jconsole這類的工具對java進程進行監(jiān)控來找到死鎖的線程,也可以使用jstack命令來排查。

首先可以用jps來找到當(dāng)前java的進程號

>jps
14804 Account  //查詢出來account這個進程的進程號
17900 Jps

使用jstack命令查詢線程運行狀態(tài)

>jstack 14804 //查看進程下所有線程狀態(tài)

2020-04-20 19:35:07
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b15 mixed mode):

"DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x0000000002fe9000 nid=0x1b2c waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE

"Thread-1" #15 prio=5 os_prio=0 tid=0x0000000020ebb000 nid=0x1e40 waiting for monitor entry [0x0000000021eff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at locktest.Account1.transfer(Account1.java:18)
        - waiting to lock <0x000000076b613c20> (a locktest.Account1)
        - locked <0x000000076b613c30> (a locktest.Account1)
        at locktest.Account1$2.run(Account1.java:39)
        at java.lang.Thread.run(Thread.java:745)

"Thread-0" #14 prio=5 os_prio=0 tid=0x00000000206ab000 nid=0x1578 waiting for monitor entry [0x0000000021dff000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at locktest.Account1.transfer(Account1.java:18)
        - waiting to lock <0x000000076b613c30> (a locktest.Account1)
        - locked <0x000000076b613c20> (a locktest.Account1)
        at locktest.Account1$1.run(Account1.java:34)
        at java.lang.Thread.run(Thread.java:745)


"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001e673800 nid=0x24c8 in Object.wait() [0x000000001f9cf000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076af08ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
        - locked <0x000000076af08ee0> (a java.lang.ref.ReferenceQueue$Lock)
        at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
        at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)

"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000000001cf90000 nid=0x2acc in Object.wait() [0x000000001f8cf000]
   java.lang.Thread.State: WAITING (on object monitor)
        at java.lang.Object.wait(Native Method)
        - waiting on <0x000000076af06b50> (a java.lang.ref.Reference$Lock)
        at java.lang.Object.wait(Object.java:502)
        at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
        - locked <0x000000076af06b50> (a java.lang.ref.Reference$Lock)
        at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)



Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x000000001cf927d8 (object 0x000000076b613c20, a locktest.Account1),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x000000001cf93e88 (object 0x000000076b613c30, a locktest.Account1),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at locktest.Account1.transfer(Account1.java:18)
        - waiting to lock <0x000000076b613c20> (a locktest.Account1)
        - locked <0x000000076b613c30> (a locktest.Account1)
        at locktest.Account1$2.run(Account1.java:39)
        at java.lang.Thread.run(Thread.java:745)
"Thread-0":
        at locktest.Account1.transfer(Account1.java:18)
        - waiting to lock <0x000000076b613c30> (a locktest.Account1)
        - locked <0x000000076b613c20> (a locktest.Account1)
        at locktest.Account1$1.run(Account1.java:34)
        at java.lang.Thread.run(Thread.java:745)

Found 1 deadlock.

可以看到jstack直接就幫我們找到了deadlock。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、競態(tài)條件: 定義:競態(tài)條件指的是一種特殊的情況,在這種情況下各個執(zhí)行單元以一種沒有邏輯的順序執(zhí)行動作,從而導(dǎo)致...
    Hughman閱讀 1,439評論 0 7
  • 死鎖產(chǎn)生的原因和解鎖的方法 產(chǎn)生死鎖的四個必要條件: (1) 互斥條件:一個資源每次只能被一個進程使用。 (2) ...
    憩在河岸上的魚丶閱讀 1,544評論 0 4
  • 產(chǎn)生死鎖的四個必要條件: (1) 互斥條件:一個資源每次只能被一個進程使用。 (2) 請求與保持條件:一個進程因請...
    像敏銳的狗閱讀 1,113評論 0 0
  • 1. cpu通過時間片分配算法來循環(huán)執(zhí)行任務(wù),當(dāng)前任務(wù)執(zhí)行一個時間片后會切換到下一任務(wù)。但是,再切換之前會保存上一...
    冰與河豚魚閱讀 748評論 0 0
  • 一擴展javalangThread類二實現(xiàn)javalangRunnable接口三Thread和Runnable的區(qū)...
    和帥_db6a閱讀 594評論 0 1

友情鏈接更多精彩內(nèi)容