Java併發程式設計實戰 04死鎖了怎麼辦?
阿新 • • 發佈:2020-05-12
# Java併發程式設計文章系列
[Java併發程式設計實戰 01併發程式設計的Bug源頭](https://mp.weixin.qq.com/s/QT44HS47l_ir08pCZeFU5Q)
[Java併發程式設計實戰 02Java如何解決可見性和有序性問題](https://mp.weixin.qq.com/s/Ryud9nizdqWI25CMLL3E_g)
[Java併發程式設計實戰 03互斥鎖 解決原子性問題](https://mp.weixin.qq.com/s/B07f7qG7rC98Ge8JSndS2Q)
# 前提
在第三篇文章最後的例子當中,需要獲取到兩個賬戶的鎖後進行轉賬操作,這種情況有可能會發生死鎖,我把上一章的程式碼片段放到下面:
```java
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`轉賬給`賬戶B`100元,`賬戶B`同時也轉賬給`賬戶A`100元,當`賬戶A`轉帳的執行緒A執行到了程式碼`(1)`處時,獲取到了`賬戶A`物件的鎖,同時`賬戶B`轉賬的執行緒B也執行到了程式碼`(1)`處時,獲取到了`賬戶B`物件的鎖。當執行緒A和執行緒B執行到了程式碼`(2)`處時,他們都在互相等待對方釋放鎖來獲取,可是`synchronized`是阻塞鎖,沒有執行完程式碼塊是不會釋放鎖的,就這樣,執行緒A和執行緒B死死的對著,誰也不放過誰。等到了你去重啟應用的那一天。。。這個現象就是`死鎖`。
**死鎖的定義:一組互相競爭資源的執行緒因互相等待,導致“永久”阻塞的現象。**
如下圖:
![死鎖1.jpg](http://qiniuyun.colablog.cn/387488cc-a4e8-438c-9f12-7b09590c9540.jpg)
# 查詢死鎖資訊
這裡我先以一個基本會發生死鎖的程式為例,建立兩個執行緒,執行緒A獲取到鎖A後,休眠1秒後去獲取鎖B;執行緒B獲取到鎖B後 ,休眠1秒後去獲取鎖A。那麼這樣基本都會發生死鎖的現象,程式碼如下:
```java
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由小到大來排序。還是用轉賬的例子來解釋,程式碼如下:
```java
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 `。程式碼如下:
```java
/** 鎖分配器(單例類) */
public class LockAllocator {
private final List