天天看點

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

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

Java并發程式設計文章系列#

Java并發程式設計實戰 01并發程式設計的Bug源頭

Java并發程式設計實戰 02Java如何解決可見性和有序性問題

Java并發程式設計實戰 03互斥鎖 解決原子性問題

前提#

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

Copy

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死死的對着,誰也不放過誰。等到了你去重新開機應用的那一天。。。這個現象就是死鎖。

死鎖的定義:一組互相競争資源的線程因互相等待,導緻“永久”阻塞的現象。

如下圖:

查找死鎖資訊#

這裡我先以一個基本會發生死鎖的程式為例,建立兩個線程,線程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 FilesJavajdk1.8.0_221bin>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 FilesJavajdk1.8.0_221bin>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]

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由小到大來排序。還是用轉賬的例子來解釋,代碼如下:

// 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);
}           
// 餘額
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程式會産生死鎖?如何定位、修複?

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

作者: Johnson木木

出處:

https://www.cnblogs.com/Johnson-lin/p/12874009.html