Java併發程式設計實戰 04死鎖了怎麼辦?

Johnson木木發表於2020-05-12

Java併發程式設計文章系列

Java併發程式設計實戰 01併發程式設計的Bug源頭
Java併發程式設計實戰 02Java如何解決可見性和有序性問題
Java併發程式設計實戰 03互斥鎖 解決原子性問題

前提

在第三篇文章最後的例子當中,需要獲取到兩個賬戶的鎖後進行轉賬操作,這種情況有可能會發生死鎖,我把上一章的程式碼片段放到下面:

public class Account {
    // 餘額
    private Long money;
    public synchronized void transfer(Account target, Long money) {
        synchronized(this) {           (1)
            synchronized (target) {    (2)
                this.money -= money;
                if (this.money < 0) {
                    // throw exception
                }
                target.money += money;
            }
        }
    }
}

賬戶A轉賬給賬戶B100元,賬戶B同時也轉賬給賬戶A100元,當賬戶A轉帳的執行緒A執行到了程式碼(1)處時,獲取到了賬戶A物件的鎖,同時賬戶B轉賬的執行緒B也執行到了程式碼(1)處時,獲取到了賬戶B物件的鎖。當執行緒A和執行緒B執行到了程式碼(2)處時,他們都在互相等待對方釋放鎖來獲取,可是synchronized是阻塞鎖,沒有執行完程式碼塊是不會釋放鎖的,就這樣,執行緒A和執行緒B死死的對著,誰也不放過誰。等到了你去重啟應用的那一天。。。這個現象就是死鎖
死鎖的定義:一組互相競爭資源的執行緒因互相等待,導致“永久”阻塞的現象。
如下圖:
死鎖1.jpg

查詢死鎖資訊

這裡我先以一個基本會發生死鎖的程式為例,建立兩個執行緒,執行緒A獲取到鎖A後,休眠1秒後去獲取鎖B;執行緒B獲取到鎖B後 ,休眠1秒後去獲取鎖A。那麼這樣基本都會發生死鎖的現象,程式碼如下:

public class DeadLock extends Thread {
    private String first;
    private String second;
    public DeadLock(String name, String first, String second) {
        super(name); // 執行緒名
        this.first = first;
        this.second = second;
    }

    public  void run() {
        synchronized (first) {
            System.out.println(this.getName() + " 獲取到鎖: " + first);
            try {
                Thread.sleep(1000L); //執行緒休眠1秒
                synchronized (second) {
                    System.out.println(this.getName() + " 獲取到鎖: " + second);
                }
            } catch (InterruptedException e) {
                // Do nothing
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        String lockA = "lockA";
        String lockB = "lockB";
        DeadLock threadA = new DeadLock("ThreadA", lockA, lockB);
        DeadLock threadB = new DeadLock("ThreadB", lockB, lockA);
        threadA.start();
        threadB.start();
        threadA.join(); //等待執行緒1執行完
        threadB.join();
    }
}

執行程式後將發生死鎖,然後使用jps命令(jps.exe在jdk/bin目錄下),命令如下:

C:\Program Files\Java\jdk1.8.0_221\bin>jps -l
24416 sun.tools.jps.Jps
24480 org.jetbrains.kotlin.daemon.KotlinCompileDaemon
1624
20360 org.jetbrains.jps.cmdline.Launcher
9256
9320 page2.DeadLock
18188

可以發現發生死鎖的程式id 9320,然後使用jstack(jstack.exe在jdk/bin目錄下)命令檢視死鎖資訊。

C:\Program Files\Java\jdk1.8.0_221\bin>jstack 9320
"ThreadB" #13 prio=5 os_prio=0 tid=0x000000001e48c800 nid=0x51f8 waiting for monitor entry [0x000000001f38f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at page2.DeadLock.run(DeadLock.java:19)
        - waiting to lock <0x000000076b99c198> (a java.lang.String)
        - locked <0x000000076b99c1d0> (a java.lang.String)

"ThreadA" #12 prio=5 os_prio=0 tid=0x000000001e48c000 nid=0x3358 waiting for monitor entry [0x000000001f28f000]
   java.lang.Thread.State: BLOCKED (on object monitor)
        at page2.DeadLock.run(DeadLock.java:19)
        - waiting to lock <0x000000076b99c1d0> (a java.lang.String)
        - locked <0x000000076b99c198> (a java.lang.String)

這樣我們就可以看到發生死鎖的資訊。雖然發現了死鎖,但是解決死鎖只能是重啟應用了。

如何避免死鎖的發生

1.固定的順序來獲得鎖

如果所有執行緒以固定的順序來獲得鎖,那麼在程式中就不會出現鎖順序死鎖問題。(取自《Java併發程式設計實戰》一書)
要想驗證鎖順序的一致性,有很多種方式,如果鎖定的物件含有遞增的id欄位(唯一、不可變、具有可比性的),那麼就好辦多了,獲取鎖的順序以id由小到大來排序。還是用轉賬的例子來解釋,程式碼如下:

public class Account {
    // id (遞增)
    private Integer id;
    // 餘額
    private Long money;
    public synchronized void transfer(Account target, Long money) {
        Account account1;
        Account account2;
        if (this.id < target.id) {
            account1 = this;
            account2 = target;
        } else {
            account1 = target;
            account2 = this;
        }

        synchronized(account1) {
            synchronized (account2) {
                this.money -= money;
                if (this.money < 0) {
                    // throw exception
                }
                target.money += money;
            }
        }
    }
}

若該物件並沒有唯一、不可變、具有可比性的的欄位(如:遞增的id),那麼可以使用 System.identityHashCode() 方法返回的雜湊值來進行比較。比較方式可以和上面的例子一類似。System.identityHashCode()雖然會出現雜湊衝突,但是發生衝突的概率是非常低的。因此這項技術以最小的代價,換來了最大的安全性。
提示: 不管你是否重寫了物件的hashCode方法,System.identityHashCode() 方法都只會返回預設的雜湊值。

2.一次性申請所有資源

只要同時獲取到轉出賬戶和轉入賬戶的資源鎖。執行完轉賬操作後,也同時釋放轉入賬戶和轉出賬戶的資源鎖。那麼則不會出現死鎖。但是使用synchronized只能同時鎖定一個資源鎖,所以需要建立一個鎖分配器LockAllocator 。程式碼如下:

/** 鎖分配器(單例類) */
public class LockAllocator {
    private final List<Object> lock = new ArrayList<Object>();
    /** 同時申請鎖資源 */
    public synchronized boolean lock(Object object1, Object object2) {
        if (lock.contains(object1) || lock.contains(object2)) {
            return false;
        }

        lock.add(object1);
        lock.add(object2);
        return true;
    }
    /** 同時釋放資源鎖 */
    public synchronized void unlock(Object object1, Object object2) {
        lock.remove(object1);
        lock.remove(object2);
    }
}

public class Account {
    // 餘額
    private Long money;
    // 鎖分配器
    private LockAllocator lockAllocator;
    
    public void transfer(Account target, Long money) {
        try {
            // 迴圈獲取鎖,直到獲取成功
            while (!lockAllocator.lock(this, target)) {
            }

            synchronized (this){
                synchronized (target){
                    this.money -= money;
                    if (this.money < 0) {
                        // throw exception
                    }
                    target.money += money;
                }
            }
        } finally {
            // 釋放鎖
            lockAllocator.unlock(this, target);
        }
    }
}

使用while迴圈不斷的去獲取鎖,一直到獲取成功,當然你也可以設定獲取失敗後休眠xx毫秒後獲取,或者其他優化的方式。釋放鎖必須使用try-finally的方式來釋放鎖。避免釋放鎖失敗。

3.嘗試獲取鎖資源

在Java中,Lock介面定義了一組抽象的加鎖操作。與內建鎖synchronized不同,使用內建鎖時,只要沒有獲取到鎖,就會死等下去,而顯示鎖Lock提供了一種無條件的、可輪詢的、定時的以及可中斷的鎖獲取操作,所有加鎖和解鎖操作都是顯示的(內建鎖synchronized的加鎖和解鎖操作都是隱示的),這篇文章就不展開來講顯示鎖Lock了(當然感興趣的朋友可以先百度一下)。

總結

在生產環境發生死鎖可是一個很嚴重的問題,雖說重啟應用來解決死鎖,但是畢竟是生產環境,代價很大,而且重啟應用後還是可能會發生死鎖,所以在編寫併發程式時需要非常嚴謹的避免死鎖的發生。避免死鎖的方案應該還有更多,鄙人不才,暫知這些方案。若有其它方案可以留言告知。非常感謝你的閱讀,謝謝。

參考文章:
《Java併發程式設計實戰》第10章
極客時間:Java併發程式設計實戰 05:一不小心死鎖了,怎麼辦?
極客時間:Java核心技術面試精講 18:什麼情況下Java程式會產生死鎖?如何定位、修復?

個人部落格網址: https://colablog.cn/

如果我的文章幫助到您,可以關注我的微信公眾號,第一時間分享文章給您
微信公眾號

相關文章