前言
只有光頭才能變強
回顧前面:
- ThreadLocal就是這麼簡單
- 多執行緒三分鐘就可以入個門了!
- 多執行緒基礎必要知識點!看了學習多執行緒事半功倍
- Java鎖機制瞭解一下
- AQS簡簡單單過一遍
- Lock鎖子類瞭解一下
- 執行緒池你真不來了解一下嗎?
本篇主要是講解死鎖,這是我在多執行緒的最後一篇了。主要將多執行緒的基礎過一遍,以後有機會再繼續深入!
死鎖是在多執行緒中也是比較重要的知識點了!
那麼接下來就開始吧,如果文章有錯誤的地方請大家多多包涵,不吝在評論區指正哦~
宣告:本文使用JDK1.8
一、死鎖講解
在Java中使用多執行緒,就會有可能導致死鎖問題。死鎖會讓程式一直卡住,不再程式往下執行。我們只能通過中止並重啟的方式來讓程式重新執行。
- 這是我們非常不願意看到的一種現象,我們要儘可能避免死鎖的情況發生!
造成死鎖的原因可以概括成三句話:
- 當前執行緒擁有其他執行緒需要的資源
- 當前執行緒等待其他執行緒已擁有的資源
- 都不放棄自己擁有的資源
1.1鎖順序死鎖
首先我們來看一下最簡單的死鎖(鎖順序死鎖)是怎麼樣發生的:
public class LeftRightDeadlock {
private final Object left = new Object();
private final Object right = new Object();
public void leftRight() {
// 得到left鎖
synchronized (left) {
// 得到right鎖
synchronized (right) {
doSomething();
}
}
}
public void rightLeft() {
// 得到right鎖
synchronized (right) {
// 得到left鎖
synchronized (left) {
doSomethingElse();
}
}
}
}
複製程式碼
我們的執行緒是交錯執行的,那麼就很有可能出現以下的情況:
- 執行緒A呼叫
leftRight()
方法,得到left鎖 - 同時執行緒B呼叫
rightLeft()
方法,得到right鎖 - 執行緒A和執行緒B都繼續執行,此時執行緒A需要right鎖才能繼續往下執行。此時執行緒B需要left鎖才能繼續往下執行。
- 但是:執行緒A的left鎖並沒有釋放,執行緒B的right鎖也沒有釋放。
- 所以他們都只能等待,而這種等待是無期限的-->永久等待-->死鎖
1.2動態鎖順序死鎖
我們看一下下面的例子,你認為會發生死鎖嗎?
// 轉賬
public static void transferMoney(Account fromAccount,
Account toAccount,
DollarAmount amount)
throws InsufficientFundsException {
// 鎖定匯賬賬戶
synchronized (fromAccount) {
// 鎖定來賬賬戶
synchronized (toAccount) {
// 判餘額是否大於0
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException();
} else {
// 匯賬賬戶減錢
fromAccount.debit(amount);
// 來賬賬戶增錢
toAccount.credit(amount);
}
}
}
}
複製程式碼
上面的程式碼看起來是沒有問題的:鎖定兩個賬戶來判斷餘額是否充足才進行轉賬!
但是,同樣有可能會發生死鎖:
- 如果兩個執行緒同時呼叫
transferMoney()
- 執行緒A從X賬戶向Y賬戶轉賬
- 執行緒B從賬戶Y向賬戶X轉賬
- 那麼就會發生死鎖。
A:transferMoney(myAccount,yourAccount,10);
B:transferMoney(yourAccount,myAccount,20);
複製程式碼
1.3協作物件之間發生死鎖
我們來看一下下面的例子:
public class CooperatingDeadlock {
// Warning: deadlock-prone!
class Taxi {
@GuardedBy("this") private Point location, destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation() {
return location;
}
// setLocation 需要Taxi內建鎖
public synchronized void setLocation(Point location) {
this.location = location;
if (location.equals(destination))
// 呼叫notifyAvailable()需要Dispatcher內建鎖
dispatcher.notifyAvailable(this);
}
public synchronized Point getDestination() {
return destination;
}
public synchronized void setDestination(Point destination) {
this.destination = destination;
}
}
class Dispatcher {
@GuardedBy("this") private final Set<Taxi> taxis;
@GuardedBy("this") private final Set<Taxi> availableTaxis;
public Dispatcher() {
taxis = new HashSet<Taxi>();
availableTaxis = new HashSet<Taxi>();
}
public synchronized void notifyAvailable(Taxi taxi) {
availableTaxis.add(taxi);
}
// 呼叫getImage()需要Dispatcher內建鎖
public synchronized Image getImage() {
Image image = new Image();
for (Taxi t : taxis)
// 呼叫getLocation()需要Taxi內建鎖
image.drawMarker(t.getLocation());
return image;
}
}
class Image {
public void drawMarker(Point p) {
}
}
}
複製程式碼
上面的getImage()
和setLocation(Point location)
都需要獲取兩個鎖的
- 並且在操作途中是沒有釋放鎖的
這就是隱式獲取兩個鎖(物件之間協作)..
這種方式也很容易就造成死鎖.....
二、避免死鎖的方法
避免死鎖可以概括成三種方法:
- 固定加鎖的順序(針對鎖順序死鎖)
- 開放呼叫(針對物件之間協作造成的死鎖)
- 使用定時鎖-->
tryLock()
- 如果等待獲取鎖時間超時,則丟擲異常而不是一直等待!
2.1固定鎖順序避免死鎖
上面transferMoney()
發生死鎖的原因是因為加鎖順序不一致而出現的~
- 正如書上所說的:如果所有執行緒以固定的順序來獲得鎖,那麼程式中就不會出現鎖順序死鎖問題!
那麼上面的例子我們就可以改造成這樣子:
public class InduceLockOrder {
// 額外的鎖、避免兩個物件hash值相等的情況(即使很少)
private static final Object tieLock = new Object();
public void transferMoney(final Account fromAcct,
final Account toAcct,
final DollarAmount amount)
throws InsufficientFundsException {
class Helper {
public void transfer() throws InsufficientFundsException {
if (fromAcct.getBalance().compareTo(amount) < 0)
throw new InsufficientFundsException();
else {
fromAcct.debit(amount);
toAcct.credit(amount);
}
}
}
// 得到鎖的hash值
int fromHash = System.identityHashCode(fromAcct);
int toHash = System.identityHashCode(toAcct);
// 根據hash值來上鎖
if (fromHash < toHash) {
synchronized (fromAcct) {
synchronized (toAcct) {
new Helper().transfer();
}
}
} else if (fromHash > toHash) {// 根據hash值來上鎖
synchronized (toAcct) {
synchronized (fromAcct) {
new Helper().transfer();
}
}
} else {// 額外的鎖、避免兩個物件hash值相等的情況(即使很少)
synchronized (tieLock) {
synchronized (fromAcct) {
synchronized (toAcct) {
new Helper().transfer();
}
}
}
}
}
}
複製程式碼
得到對應的hash值來固定加鎖的順序,這樣我們就不會發生死鎖的問題了!
2.2開放呼叫避免死鎖
在協作物件之間發生死鎖的例子中,主要是因為在呼叫某個方法時就需要持有鎖,並且在方法內部也呼叫了其他帶鎖的方法!
- 如果在呼叫某個方法時不需要持有鎖,那麼這種呼叫被稱為開放呼叫!
我們可以這樣來改造:
- 同步程式碼塊最好僅被用於保護那些涉及共享狀態的操作!
class CooperatingNoDeadlock {
@ThreadSafe
class Taxi {
@GuardedBy("this") private Point location, destination;
private final Dispatcher dispatcher;
public Taxi(Dispatcher dispatcher) {
this.dispatcher = dispatcher;
}
public synchronized Point getLocation() {
return location;
}
public synchronized void setLocation(Point location) {
boolean reachedDestination;
// 加Taxi內建鎖
synchronized (this) {
this.location = location;
reachedDestination = location.equals(destination);
}
// 執行同步程式碼塊後完畢,釋放鎖
if (reachedDestination)
// 加Dispatcher內建鎖
dispatcher.notifyAvailable(this);
}
public synchronized Point getDestination() {
return destination;
}
public synchronized void setDestination(Point destination) {
this.destination = destination;
}
}
@ThreadSafe
class Dispatcher {
@GuardedBy("this") private final Set<Taxi> taxis;
@GuardedBy("this") private final Set<Taxi> availableTaxis;
public Dispatcher() {
taxis = new HashSet<Taxi>();
availableTaxis = new HashSet<Taxi>();
}
public synchronized void notifyAvailable(Taxi taxi) {
availableTaxis.add(taxi);
}
public Image getImage() {
Set<Taxi> copy;
// Dispatcher內建鎖
synchronized (this) {
copy = new HashSet<Taxi>(taxis);
}
// 執行同步程式碼塊後完畢,釋放鎖
Image image = new Image();
for (Taxi t : copy)
// 加Taix內建鎖
image.drawMarker(t.getLocation());
return image;
}
}
class Image {
public void drawMarker(Point p) {
}
}
}
複製程式碼
使用開放呼叫是非常好的一種方式,應該儘量使用它~
2.3使用定時鎖
使用顯式Lock鎖,在獲取鎖時使用tryLock()
方法。當等待超過時限的時候,tryLock()
不會一直等待,而是返回錯誤資訊。
使用tryLock()
能夠有效避免死鎖問題~~
2.4死鎖檢測
雖然造成死鎖的原因是因為我們設計得不夠好,但是可能寫程式碼的時候不知道哪裡發生了死鎖。
JDK提供了兩種方式來給我們檢測:
- JconsoleJDK自帶的圖形化介面工具,使用JDK給我們的的工具JConsole
- Jstack是JDK自帶的命令列工具,主要用於執行緒Dump分析。
具體可參考:
三、總結
發生死鎖的原因主要由於:
- 執行緒之間交錯執行
- 解決:以固定的順序加鎖
- 執行某方法時就需要持有鎖,且不釋放
- 解決:縮減同步程式碼塊範圍,最好僅操作共享變數時才加鎖
- 永久等待
- 解決:使用
tryLock()
定時鎖,超過時限則返回錯誤資訊
- 解決:使用
在作業系統層面上看待死鎖問題(這是我之前做的筆記、很淺顯):
參考資料:
- 《Java核心技術卷一》
- 《Java併發程式設計實戰》
- 《計算機作業系統 湯小丹》
如果文章有錯的地方歡迎指正,大家互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同學,可以關注微信公眾號:Java3y。
文章的目錄導航: