思維導圖:
引言:
本文的主要內容是介紹一些導致程序發生活躍性故障的原因,以及如何避免他們的方法。
- 理論部分:介紹死鎖,活鎖,飢餓這些活躍性故障發生的原因及避免方法。
一.死鎖
有個經典的哲學家問題,有五個哲學家,每兩個哲學家之間有一根筷子,當哲學家擁有兩根筷子時就可以吃飯。這其中就有發生死鎖的情況,即每個哲學家都佔有一根筷子並等待其他人放下筷子,結果就是所有的哲學家都吃不着飯。
所以,什麼情況下會產生死鎖呢?當每個人都擁有其他人需要的資源,同時又等待其他人擁有的資源,並且每個人在獲取所需要的資源之前又不肯放棄已擁有的資源,就會產生死鎖。
接下來我們介紹部分死鎖的類型以及如何避免發生死鎖。
1.1 死鎖類型
1.1.1 鎖順序死鎖
由於多個線程不正確的獲取鎖的順序而導致的死鎖。比如如下例子,線程A執行leftRight方法可能已獲取left的鎖並等待獲取right的鎖。線程B則執行rightLeft方法並已獲取right的鎖並等待獲取left的鎖,此時,這兩個線程就會發生死鎖。
避免產生鎖順序死鎖的方法則是將兩個方法獲取鎖的順序保持一致,都首先獲取left的鎖,或者都首先獲取right的鎖,就可以避免死鎖的發生。
public class LeftRightDeadlock {
private final Object left = new Object();
private final Object right = new Object();
public void leftRight() {
synchronized (left) {
synchronized (right) {
doSomething();
}
}
}
public void rightLeft() {
synchronized (right) {
synchronized (left) {
doSomethingElse();
}
}
}
void doSomething() {
}
void doSomethingElse() {
}
}
1.1.2 動態的鎖順序死鎖
有時候,方法對鎖的獲取順序並不是固定的,有可能是動態變化的。如下這個例子。當線程A將把賬戶1的錢轉向賬戶2,同時線程B又將把賬戶2的錢轉向賬戶1的話,就有可能發生死鎖。
解決辦法則是在方法內部對獲取鎖的方式進行固定。例如,每個賬戶一般都有一個唯一性的賬號,我們可以通過賬號的大小來對鎖的獲取進行排序,以避免動態的鎖順序死鎖的發生。
public class DynamicOrderDeadlock {
// 可能發生死鎖
public static void transferMoney(Account fromAccount, Account toAccount, DollarAmount amount) throws InsufficientFundsException {
synchronized (fromAccount) {
synchronized (toAccount) {
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException();
} else {
fromAccount.debit(amount);
toAccount.credit(amount);
}
}
}
}
static class DollarAmount implements Comparable<DollarAmount> {
// Needs implementation
public DollarAmount(int amount) {
}
public DollarAmount add(DollarAmount d) {
return null;
}
public DollarAmount subtract(DollarAmount d) {
return null;
}
public int compareTo(DollarAmount dollarAmount) {
return 0;
}
}
static class Account {
private DollarAmount balance;
private final int acctNo;
private static final AtomicInteger sequence = new AtomicInteger();
public Account() {
acctNo = sequence.incrementAndGet();
}
void debit(DollarAmount d) {
balance = balance.subtract(d);
}
void credit(DollarAmount d) {
balance = balance.add(d);
}
DollarAmount getBalance() {
return balance;
}
int getAcctNo() {
return acctNo;
}
}
static class InsufficientFundsException extends Exception {
}
}
1.1.3 在協作對象之間發生死鎖
死鎖的發生並不會僅僅出現在同一個類的某個方法之中,就比如以上兩個例子,而是有可能出現在多個類的多個方法的互相調用之中,比如以下這個例子。Taxi代表出租車,Dispatcher代表出租車車隊。
線程A如果調用Taxe的setLocation方法的話,首先需要獲取Taxi的鎖,因爲setLocation方法又使用了Dispatcher的notifyAvailable方法,所以還需要獲取Dispatcher的鎖。但是,如果此時,有個線程B,調用了Dispatcher的getImage方法,首先 會獲取Dispatcher的鎖,然後,在getImage方法之中,又會獲取Taxi的鎖,結果就是產生死鎖。
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;
}
public synchronized void setLocation(Point location) {
this.location = location;
if (location.equals(destination)) {
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);
}
public synchronized Image getImage() {
Image image = new Image();
for (Taxi t : taxis)
image.drawMarker(t.getLocation());
return image;
}
}
class Image {
public void drawMarker(Point p) {
}
}
}
避免在對象協作是產生死鎖的方式則是使用開放調用,即在調用其他方法的時候不在持有某個對象的鎖。如下例,在調用其他方法的時候,對持有的Taxi或Dispatcher的鎖進行釋放。
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;
synchronized (this) {
this.location = location;
reachedDestination = location.equals(destination);
}
if (reachedDestination) {
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;
synchronized (this) {
copy = new HashSet<Taxi>(taxis);
}
Image image = new Image();
for (Taxi t : copy)
image.drawMarker(t.getLocation());
return image;
}
}
class Image {
public void drawMarker(Point p) {
}
}
}
1.1.4 資源死鎖
舉例,在某種比較極端的情況下。兩個數據庫連接池都只有一個連接可用。若線程A,B都需要使用這兩個數據庫連接池的唯一一個連接,而且獲取連接的順序不同的話,就有可能產生資源死鎖。
線程飢餓死鎖也是資源死鎖的一種形式。例如,在單線程線程池中,正在執行的任務A需要任務B的結果,但是任務B又必須等待任務A執行完畢時才能執行,這就會導致線程飢餓死鎖。
1.2 死鎖的避免
在第一小節中已經介紹過一些避免死鎖的方法,這一小節則進行總結,並添加一些額外的沒有介紹到的方法以避免和診斷死鎖。
- 加鎖順序固定:固定加鎖的順序以防止方法內死鎖的發生
- 開放調用:在調用其他方法前釋放已獲取的鎖,以避免死鎖的發生
- 支持定時的鎖:使用顯示鎖Lock並設置超時時間,以從死鎖中恢復。
- 線程轉儲信息:可以利用線程轉儲信息分析死鎖發生的原因
二.活鎖
活鎖的表現形式是不會阻塞線程,但也不能繼續執行線程,因爲線程將不斷重複執行相同的操作,而且總是會失敗。
當多個相互協作的線程都對彼此進行響應從而修改各自的狀態,並使得任何一個線程都無法繼續執行時,就發生了活鎖。活鎖通常是由過度的錯誤恢復代碼造成的。
解決活鎖問題一般需要在重試時引入某種隨機性。就好像在數據通信時,如果兩臺機器使用相同的載波來發送數據包,這些數據包就會發生衝突,如果這兩個數據包都在固定的時間後嘗試重新發送,那麼肯定還是會發生衝突,但是如果這兩個數據包都在一個有界限的隨機的時間後嘗試重新發送,那麼衝突的可能性就基本沒有了。
三.飢餓
當線程由於無法訪問它所需要的資源而不能繼續執行時,就發生了飢餓。
引發飢餓最常見的資源就是CPU時鐘週期。如果Java優先級使用不當,或者在持有鎖時執行一些無法結束的結構(例如無限循環,無限制的等待某個資源),就有可能導致飢餓的發生。
避免飢餓的方式是,不要嘗試修改線程的優先級,使用默認優先級就好。修改線程優先級不僅可能會導致飢餓而且會增加平臺的依賴性。