第十章 避免活躍性危險
安全性和活躍性之間存在某種制衡:
使用加鎖機制來確保線程安全溃斋,但如果過度使用加鎖阿弃,則可能導(dǎo)致鎖順序死鎖(Lock-Ordering Deadlock)江咳。同樣晒哄,使用線程池和信號量來限制對資源的使用被芳,但這些被限制的行為可能會導(dǎo)致資源死鎖(Resource Deadlock)
10.1 死鎖
經(jīng)典的“哲學(xué)家進餐”問題很好地描述了死鎖狀況巨柒。
5個哲學(xué)家圍坐在一個圓桌上樱拴,每兩個哲學(xué)家之間都有一只筷子柠衍,哲學(xué)家平時進行思考,只有當他們饑餓時晶乔,才拿起筷子吃飯珍坊。規(guī)定每個哲學(xué)家只能先取其左邊筷子,然后取其右邊筷子正罢,然后才可以吃飯阵漏。如果5個哲學(xué)家同時拿起自己左邊的筷子,就會發(fā)生死鎖翻具。每個人都擁有其他人需要的資源履怯,同時又等待其他人已經(jīng)擁有的資源,并且每個人在獲得所有需要的資源之前都不會放棄已經(jīng)擁有的資源裆泳。
當一個線程永遠地持有一個鎖叹洲,并且其他線程都嘗試獲得這個鎖時,那么它們將永遠被阻塞工禾。這種情況就是最簡單的死鎖形式(稱為抱死[Deadly Embrace]),其中多個線程由于存在環(huán)路的鎖依賴關(guān)系而永遠等待下去疹味。(把每個線程假想為有向圖的一個節(jié)點,圖中每條邊表示的關(guān)系是:“線程A等待線程B所占有的資源”帜篇。如果圖中形成一條環(huán)路,那么就存在一個死鎖)诫咱。
當一組Java線程發(fā)生死鎖時笙隙,這些線程永遠不能再使用。根據(jù)線程完成工作的不同坎缭,可能造成應(yīng)用程序完全停止竟痰,或者某個特定的子系統(tǒng)停止,或者時候性能降低掏呼』悼欤恢復(fù)應(yīng)用程序的唯一方式就是中止并重啟它。
10.1.1 鎖順序死鎖
程序清單10-1中的LeftRightDeadlock存在死鎖風險憎夷。
程序清單 10-1
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() {
}
}
leftRight和rightLeft這兩個方法分別獲得left鎖和right鎖莽鸿。如果一個線程調(diào)用了leftRight,另一個線程調(diào)用了rightLeft拾给,并且兩個線程的操作是交錯執(zhí)行的祥得,會發(fā)生死鎖,如圖:
發(fā)生死鎖的原因是:兩個線程 試圖以不同的順序來獲取相同的鎖蒋得。如果按照相同的順序來請求鎖级及,就不會出現(xiàn)循環(huán)的加鎖依賴性,就不會產(chǎn)生死鎖额衙。
如果所有線程以固定的順序來獲得鎖饮焦,那么在程序中就不會出現(xiàn)鎖順序死鎖問題怕吴。
10.1.2 動態(tài)的鎖順序死鎖
考慮10-2中的代碼,它將資金從一個賬戶轉(zhuǎn)入到另一個賬戶县踢。在開始轉(zhuǎn)賬之前转绷,首先要獲得這兩個Account對象的鎖,以卻不通過原子方式來更新兩個賬戶中的余額殿雪,同時又不能破壞一些不變性條件暇咆,例如“賬戶的余額不能為負數(shù)”。
程序清單 10-2
public class DynamicOrderDeadlock {
// Warning: deadlock-prone!
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 {
}
}
所有的線程似乎按相同的順序來獲得鎖丙曙,但事實上鎖的順序取決與傳遞給transferMoney的參數(shù)順序爸业,而這些參數(shù)順序又取決與外部輸入。
如果兩個線程同時調(diào)用transferMoney,其中一個線程從X向Y轉(zhuǎn)賬亏镰,而另一個線程從Y向X轉(zhuǎn)賬扯旷,那么就會發(fā)生死鎖:
A: transferMoney(myAccount, yourAccount, 10);
B: transferMoney(yourAccount, myAccount, 20);
如果執(zhí)行時序不當,那么A可能獲得myAccount的鎖并等待yourAccount的鎖索抓,然而B此時擁有yourAccount的鎖并正在等到myAccount的鎖钧忽。
這種死鎖可以采用10-1中的方法來檢查——查看是否存在嵌套ed鎖獲取操作。由于我們無法控制參數(shù)的順序逼肯,因此要解決這個問題耸黑,必須定義鎖的順序,并在整個應(yīng)用程序中都按照這個順序來獲取鎖篮幢。
在制定鎖的順序時大刊,可以使用System.identityHashCode方法,該方法將返回由Object.hashCode返回的值三椿。10-3給出另一個版本的transferMoney缺菌,使用了System.identityHashCode來定義鎖的順序。
雖然加了一些新的代碼搜锰,但卻消除了死鎖的可能性伴郁。
程序清單 10-3
public class InduceLockOrder {
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);
}
}
}
int fromHash = System.identityHashCode(fromAcct);
int toHash = System.identityHashCode(toAcct);
if (fromHash < toHash) {
synchronized (fromAcct) {
synchronized (toAcct) {
new Helper().transfer();
}
}
} else if (fromHash > toHash) {
synchronized (toAcct) {
synchronized (fromAcct) {
new Helper().transfer();
}
}
} else {
synchronized (tieLock) {
synchronized (fromAcct) {
synchronized (toAcct) {
new Helper().transfer();
}
}
}
}
}
interface DollarAmount extends Comparable<DollarAmount> {
}
interface Account {
void debit(DollarAmount d);
void credit(DollarAmount d);
DollarAmount getBalance();
int getAcctNo();
}
class InsufficientFundsException extends Exception {
}
}
在極少數(shù)情況下,兩個對象可能擁有相同的散列值(HashCode)蛋叼,此時必須通過某種任意的方法來決定鎖的順序焊傅,而這可能又會重新引入死鎖。為了避免這種情況鸦列,可以使用“加時賽(Tie-Breaking)鎖”租冠。在獲得兩個Account鎖之前,首先獲得這個“加時賽”鎖薯嗤,從而保證每次只有一個線程以未知的順序得到這兩個鎖顽爹,從而消除了死鎖發(fā)生的可能性(只要一致地使用這種機制)。
如果經(jīng)常出現(xiàn)散列沖突(hash collisions)骆姐,那么這種技術(shù)可能會稱為并發(fā)性的一個瓶頸(類似與在整個程序中只有一個鎖的情況镜粤,因為經(jīng)常要等待獲得加時賽鎖)捏题,但由于System.identityHashCode中出現(xiàn)散列沖突的頻率非常低,因此這項技術(shù)以最小的代價肉渴,換來了最大的安全性公荧。
如果在Account中包含一個唯一的,不可變的同规,并且具備可比性的鍵值循狰,例如賬號,那么要指定鎖的順序就更加容易:通過鍵值對對象進行排序券勺,因而不需要使用“加時賽”鎖绪钥。
鎖被持有的時間通常很短暫,然而死鎖往往是很嚴重的問題关炼。
作為商業(yè)產(chǎn)品的應(yīng)用程序可能每天被執(zhí)行數(shù)十億次獲取鎖-釋放鎖的操作程腹,只要在這數(shù)十億次操作中有一次發(fā)生了錯誤,就可能導(dǎo)致程序發(fā)生死鎖儒拂,并且即使應(yīng)用程序通過了壓力測試也不可能找出所有潛在的死鎖(短時間持有鎖是為了降低鎖的競爭程度寸潦,卻增加了在測試中找出潛在死鎖風險的難度),10-4中的DemonstrateDeadlock在多數(shù)系統(tǒng)下很快發(fā)生死鎖社痛。
為了簡便见转,DemonstrateDeadlock沒有考慮賬戶余額來負數(shù)的問題。
程序清單 10-4
public class DemonstrateDeadlock {
private static final int NUM_THREADS = 20;
private static final int NUM_ACCOUNTS = 5;
private static final int NUM_ITERATIONS = 1000000;
public static void main(String[] args) {
final Random rnd = new Random();
final Account[] accounts = new Account[NUM_ACCOUNTS];
for (int i = 0; i < accounts.length; i++)
accounts[i] = new Account();
class TransferThread extends Thread {
public void run() {
for (int i = 0; i < NUM_ITERATIONS; i++) {
int fromAcct = rnd.nextInt(NUM_ACCOUNTS);
int toAcct = rnd.nextInt(NUM_ACCOUNTS);
DollarAmount amount = new DollarAmount(rnd.nextInt(1000));
try {
DynamicOrderDeadlock.transferMoney(accounts[fromAcct], accounts[toAcct], amount);
} catch (DynamicOrderDeadlock.InsufficientFundsException ignored) {
}
}
}
}
for (int i = 0; i < NUM_THREADS; i++)
new TransferThread().start();
}
}
上述程序調(diào)用的是程序清單10-2中的DynamicOrderDeadlock.transferMoney方法蒜哀,所以極容易出現(xiàn)死鎖池户。
10.1.3 在協(xié)作對象之間發(fā)生的死鎖
某些獲取多個鎖的操作并不想上面那么明顯,這兩個鎖不一定在同一個方法中被獲取凡怎。10-5中兩個互相協(xié)作的類,在出租車調(diào)度系統(tǒng)中可能會用到它們赊抖。Taxi代表一個出租車對象统倒,包含位置和目的地兩個屬性,Dispatcher代表一個出租車車隊氛雪。
程序清單 10-5
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) {
}
}
}
盡管沒有任何方法顯式地獲取兩個鎖房匆,但setLocation和getImage等方法的調(diào)用者都會獲得兩個鎖。如果一個線程在收到GPS接收器的更新事件時調(diào)用setLocation报亩,那么它將首先更新出租車的位置浴鸿,然后判斷它是否到達了目的地。如果到達了弦追,它會通知Dispatcher:它需要一個新目的地岳链。因為setLocation和notifyAvailable都是同步方法,因此調(diào)用setLocation的線程將首先獲得Taxi的鎖劲件,然后再獲得Dispatcher的鎖掸哑。同樣约急,調(diào)用getImage的線程將首先獲取Dispatcher鎖,然后再獲取每一個Taxi的鎖(每次獲取一個)苗分。這與LeftRightDeadlock中的情況相同厌蔽,兩個線程按照不同的順序來獲取兩個鎖,因此可能產(chǎn)生死鎖摔癣。
在LeftRightDeadlock和transferMoney中奴饮,要查找死鎖時比較簡單的:只需要找出那些需要獲取兩個鎖的方法。然而要在Taxi和Dispatcher中查找死鎖是比較困難的:如果在持有鎖的情況下需要調(diào)用某個外部方法择浊,就需要警惕死鎖戴卜。
如果在持有鎖時調(diào)用某個外部方法,那么將出現(xiàn)活躍性問題近她。在這個外部方法中可能或獲取其他鎖(這可能產(chǎn)生死鎖)叉瘩,或者阻塞時間過長,導(dǎo)致其他線程無法及時獲得當前被持有的鎖粘捎。
10.1.4 開放調(diào)用
方法調(diào)用相當于一種抽象屏障薇缅,你無需了解在調(diào)用方法中所執(zhí)行的操作,也正是由于不知道在被調(diào)用方法中執(zhí)行的操作攒磨,因此在持有鎖的時候?qū)φ{(diào)用某個外部方法將難以進行分析泳桦,從而可能出現(xiàn)死鎖。
如果在調(diào)用某個方法時不需要持有鎖娩缰,那么這種調(diào)用被稱為開放調(diào)度(Open Call)灸撰。
依賴于開放調(diào)度的類通常能表現(xiàn)出更好的行為,并且與那些在調(diào)度方法時需要持有鎖的類相比拼坎,也更易于編寫浮毯。
這種通過開放來避免死鎖的方法,類似于采用封裝機制來提供線程安全的方法:雖然在沒有封裝的情況下也能確保構(gòu)建線程安全的類泰鸡,但對一個使用了封裝的程序進行線程安全分析债蓝,要比分析沒有使用封裝的程序容易得多。
同理盛龄,分析一個完全依賴于開放調(diào)用的程序的活躍性饰迹,要比分析那些不依賴開放調(diào)用的程序的活躍性簡單。
通過盡可能地使用開放調(diào)用余舶,將更容易找出那些需要獲取多個鎖的代碼路徑啊鸭,因此也就更容易確保采用一直的順序來獲得鎖。
將10-5修改為開放調(diào)用匿值,從而消除死鎖的風險赠制,這需要使同步代碼塊僅被用于保護那些涉及共享狀態(tài)的操作,如10-6所示挟憔。
通常憎妙,如果只是為了語法緊湊或簡單性(而不是因為整個方法必須通過一個鎖來保護)而使用同步方法(而不是同步代碼塊)库正,將導(dǎo)致10-5中的問題。(收縮同步代碼塊的范圍還可以提高可伸縮性厘唾,在11.4.1中)
程序清單 10-6
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) {
}
}
}
在程序中應(yīng)盡量使用開放調(diào)用褥符,與那些在持有鎖時調(diào)用外部方法的程序相比,更容易對依賴于開放調(diào)用的程序進行死鎖分析抚垃。
有時候在重新編寫同步代碼塊以使用開放調(diào)用時會產(chǎn)生意想不到的結(jié)果喷楣,因為這會使得某個原子操作變成非原子操作。
在許多情況下鹤树,某個操作失去原子性是可以接受的铣焊。例如,對于兩個操作:更新出租車位置以及通知調(diào)度程序這輛出租車已準備好出發(fā)去一個新的目的地罕伯,這兩個操作并不需要實現(xiàn)為一個原子操作曲伊。
在其他情況中,雖然去掉原子性可能會出現(xiàn)一些值得注意的結(jié)果追他,但這中語義變化仍時候可以接受的坟募。
在容易產(chǎn)生死鎖的版本中(即10-5),getImage會生成某個時刻下的某個車隊位置的完成快照邑狸,而在重新改寫的版本中懈糯,getImage將獲得每輛出租車不同時刻的位置。
然而单雾,在某寫情況下赚哗,丟失原子性會引發(fā)錯誤,此時需要通過另一種技術(shù)來實現(xiàn)原子性硅堆。
例如屿储,在構(gòu)造一個并發(fā)對象時,使得每次只有單個線程執(zhí)行使用了開放調(diào)用的代碼路徑渐逃。
例如扩所,在關(guān)閉某個服務(wù)時,你可能希望所有正在運行的操作執(zhí)行完成以后朴乖,再釋放這些服務(wù)占用的資源。如果在等待操作完成的同時持有該服務(wù)的鎖助赞,那么將容易導(dǎo)致死鎖买羞,但如果在服務(wù)關(guān)閉之前就釋放服務(wù)的鎖,則可能導(dǎo)致其他線程開始新的操作雹食。
這個問題的解決方法是畜普,在將服務(wù)的狀態(tài)更新為“關(guān)閉”之前一直持有鎖,這樣其他想要開始新操作的線程群叶,包括想關(guān)閉該服務(wù)的其他操作吃挑,會發(fā)現(xiàn)服務(wù)已經(jīng)不可用钝荡,因此也就不會試圖開始新的操作。然后舶衬,你可以等待關(guān)閉操作結(jié)束埠通,并且知道當開放調(diào)用完成后,只有執(zhí)行關(guān)閉操作的線程才能訪問服務(wù)的狀態(tài)逛犹。因此端辱,這項技術(shù)依賴于一些協(xié)議(而不是通過加鎖)來防止其他線程來進入代碼的臨界區(qū)。
10.1.5 資源死鎖
正如當多個線程相互持有彼此正在等待的鎖而不釋放自己已持有的鎖時發(fā)生死鎖虽画,當它們在相同的資源集合上等待時舞蔽,也會發(fā)生死鎖。
假設(shè)有兩個資源池码撰,例如兩個不同數(shù)據(jù)庫的連接池渗柿。資源池通常采用信號量來實現(xiàn)(5.5.3)當資源池為空的阻塞行為。如果一個任務(wù)需要連接兩個數(shù)據(jù)庫脖岛,并且在請求這兩個資源時不會始終遵循相同的順序朵栖,那么線程A可能持有與數(shù)據(jù)庫D1的連接,并等待與數(shù)據(jù)庫D2的連接鸡岗,而線程B則持有與D2的連接并等待與D1的連接(資源池越大混槐,出現(xiàn)這種情況的可能性就越小,如果每個資源池都有N個連接轩性,那么在發(fā)生死鎖時不僅需要N個循環(huán)等待的線程声登,而且還需要大量不恰當?shù)膱?zhí)行時序)
另一種基于資源的死鎖形式就是線程饑餓死鎖(Thread-Starvation Deadlock)。
8.1.1節(jié)中給出了這種危害的一個示例:一個任務(wù)提交另一個任務(wù)揣苏,并等待被提交任務(wù)在單線程的Executor中執(zhí)行完成悯嗓。這種情況天,第一個任務(wù)將永遠等待下去卸察,并使得另一個任務(wù)以及在這個Executor中執(zhí)行的所有其他任務(wù)都停止執(zhí)行脯厨。如果某些任務(wù)需要等待其他任務(wù)的結(jié)果,那么這些任務(wù)往往時產(chǎn)生線程饑餓死鎖的主要來源坑质,有界線程池/資源池與相互依賴的任務(wù)不能一起使用合武。
10.2 死鎖的避免與診斷
如果必須獲取多個鎖,那么在設(shè)計時必須考慮鎖的順序:盡量減少潛在的加鎖 交互數(shù)量涡扼,將獲取鎖時需要遵循的協(xié)議寫入正式文檔并始終遵循這些協(xié)議晒奕。
在使用細粒度(fine-grained)鎖的程序中氏捞,可以通過使用一種兩階段策略(Two-Part Strategy)來檢查代碼中的死鎖:首先经瓷,找出在什么地方將獲取多個鎖(使這個集合盡量形:拧),然后對所有這些實例進行全局分析,從而確保它們在整個程序中獲取鎖的順序都保持一致红淡。盡可能地使用開放調(diào)用不狮,這能極大地簡化分析過程。
如果所有的調(diào)用都是開放調(diào)用在旱,那么要發(fā)現(xiàn)獲取多個鎖的實例是非常簡單的摇零,可以通過代碼審查或者借助自動化的源代碼分析工具。
10.2.1 支持定時的鎖
還有一項技術(shù)可以檢查死鎖和從死鎖中恢復(fù)過來颈渊,即顯式使用Lock類中的定時tryLock功能(13章)來代替內(nèi)置鎖機制遂黍。
當使用內(nèi)置鎖時,只要沒有獲得鎖俊嗽,就會永遠等待下去雾家,而顯式鎖則可以執(zhí)行一個超時時限(Timeout),在等待超過該事件后tryLock會返回一個失敗信息绍豁。
如果超時時限要比獲取鎖的時間要長很多芯咧,那么就可以在發(fā)生某個以外情況后重新獲得控制權(quán)(13-3給出了transferMoney的另一種實現(xiàn),其中使用了一種輪詢的tryLock消除了死鎖發(fā)生的可能性)竹揍。
當定時鎖失敗時敬飒,并不需要知道失敗的原因》椅唬或許是因為發(fā)生了死鎖无拗,或許某個線程在持有鎖時錯誤地進入了無限循環(huán),還可能是某個操作的執(zhí)行時間遠遠超出了預(yù)期昧碉。
然而英染,至少能記錄所發(fā)生的失敗,以及關(guān)于這次操作的其他有用信息被饿,并通過一種更平緩的方法來重新啟動計算四康,而不是關(guān)閉整個進程。
即使在整個系統(tǒng)中沒有始終使用定時鎖狭握,使用定時鎖來獲取多個鎖也能有效地應(yīng)對死鎖問題闪金。
如果在獲取鎖時超時,那么可以釋放這個鎖论颅,然后后退并在一段時間后再次蠶食哎垦,從而消除了死鎖發(fā)生的條件,使程序恢復(fù)過來恃疯。(這項技術(shù)只有在同時獲取兩個鎖時才有效漏设,如果在嵌套的方法調(diào)用中請求多個鎖,那么即使你知道已經(jīng)有了外層的鎖澡谭,也無法釋放它)
10.2.2 通過線程轉(zhuǎn)儲信息來分析死鎖
JVM通過線程轉(zhuǎn)儲(Thread Dump)來幫助識別死鎖的發(fā)生。
線程轉(zhuǎn)儲包括各個運行中的線程的棧追蹤信息,這類似于發(fā)生異常時的棧追蹤信息蛙奖。
線程轉(zhuǎn)儲還包含加鎖信息潘酗,例如每個線程持有了哪些鎖,在那些棧幀中獲得這些鎖雁仲,以及被阻塞的線程正在等待獲取哪一個鎖仔夺。
在生成線程轉(zhuǎn)儲之前,JVM將在等待關(guān)系圖中通過搜索循環(huán)來找出死鎖攒砖。如果發(fā)現(xiàn)了一個死鎖缸兔,則獲取相應(yīng)的死鎖信息,例如在死鎖中涉及哪些鎖和線程吹艇,以及這個鎖的獲取操作位于程序的哪些位置.
要在UNIX平臺上觸發(fā)線程轉(zhuǎn)儲操作惰蜜,可以通過向JVM的進程發(fā)送SIGQUIT信息(kill-3),或者在UNIX平臺中按下Ctrl-\鍵受神,在windows平臺中按下Ctrl-Break鍵抛猖。在許多IDE(Integrated Development Environment,集成開發(fā)環(huán)境)中都可以請求線程轉(zhuǎn)儲。
內(nèi)置鎖與獲得它們所在的線程棧幀時相關(guān)聯(lián)的鼻听,而顯式的Lock只獲得它的線程相關(guān)聯(lián)财著。
10-7給出了一個J2EE應(yīng)用程序中獲取的部分線程轉(zhuǎn)儲信息。在導(dǎo)致死鎖的故障中包括3個組件:1個J2EE應(yīng)用程序撑碴,一個J2EE容器撑教,以及一個JDBC驅(qū)動程序,分別由不同的生產(chǎn)商提供醉拓。
程序清單10-7
Found one Java-level deadlock:
=============================
"ApplicationServerThread":
waiting to lock monitor 0x080f0cdc (a MumbleDBConnection),
which is held by "ApplicationServerThread"
"ApplicationServerThread":
waiting to lock monitor 0x080f0ed4 (a MumbleDBCallableStatement),
which is held by "ApplicationServerThread"
Java stack information for the threads listed above:
"ApplicationServerThread":
at MumbleDBConnection.remove_statement
- waiting to lock <0x650f7f30> (a MumbleDBConnection)
at MumbleDBStatement.close
- locked <0x6024ffb0> (a MumbleDBCallableStatement)
...
"ApplicationServerThread":
at MumbleDBCallableStatement.sendBatch
- waiting to lock <0x6024ffb0> (a MumbleDBCallableStatement)
at MumbleDBConnection.commit
- locked <0x650f7f30> (a MumbleDBConnection)
...
我們只給出了查找死鎖相關(guān)的部分線程轉(zhuǎn)儲信息伟姐。當診斷死鎖時,JVM可以幫我們做許多工作——哪些鎖導(dǎo)致了問題廉嚼,涉及哪些線程玫镐,它們持有哪些其他的鎖,以及是否間接地給其他線程帶了不利影響怠噪。
其中一個線程持有MumbleDBConnection上的鎖恐似,并等待獲得MumbleDBCallableStatement上的鎖,而另一個線程則持有MumbleDBCallableStatement上的鎖傍念,并等待MumbleDBConnection上的鎖矫夷。
10.3 其他活躍性危險
死鎖時最常見的活躍性危險,在并發(fā)線程中還存在一些其他的活躍性危險憋槐,包括:饑餓双藕,丟失信號和活鎖等。
10.3.1 饑餓
當線程由于無法訪問它所需要的資源而不能繼續(xù)執(zhí)行時阳仔,就發(fā)生了“饑餓(Starvation)”忧陪。
引發(fā)饑餓的最常見資源就是CPU時鐘周期。如果在Java應(yīng)用程序中對線程的優(yōu)先級使用不當,或者在持有鎖時執(zhí)行一些無法結(jié)束的結(jié)構(gòu)(例如無限循環(huán)嘶摊,或無限制等待某個資源)延蟹,那么也可能導(dǎo)致饑餓,因為其他需要這個鎖的線程將無法得到它叶堆。
在Thread API定義的線程優(yōu)先級只是作為線程調(diào)度的參考阱飘。在Thread API中定義了10個優(yōu)先級,JVM根據(jù)需要將它們映射到操作系統(tǒng)的調(diào)度優(yōu)先級虱颗,這種映射時與特定平臺(不同的操作系統(tǒng))相關(guān)的沥匈。在某些操作系統(tǒng)中,如果優(yōu)先級的數(shù)量少于10個忘渔,那么有多個Java優(yōu)先級會被映射到同一個優(yōu)先級高帖。
要避免使用線程優(yōu)先級,因為這會增加平臺依賴性辨萍,并可能導(dǎo)致活躍性問題棋恼。在大多數(shù)并發(fā)應(yīng)用程序中,都可以使用默認的線程優(yōu)先級锈玉。
10.3.2 糟糕的響應(yīng)性
如果在GUI應(yīng)用程序中使用了后臺線程爪飘,那么糟糕的響應(yīng)性時是常見的。
我們在第9章開發(fā)了一個框架拉背,并把運行時間較長的任務(wù)放到后臺線程中運行师崎,從而不會使用戶界面失去響應(yīng)。但CPU密集型的后臺任務(wù)仍可能對響應(yīng)性造成影響椅棺,因為它們會與事件線程共同競爭CPU的時鐘周期犁罩。在這種情況下可以發(fā)揮線程優(yōu)先級的作用,此時計算密集型的后臺任務(wù)將對響應(yīng)性造成影響两疚。如果由其他線程完成的工作都是后臺任務(wù)床估,那么應(yīng)該降低它們的優(yōu)先級,從而提高前臺程序的響應(yīng)性诱渤。
不良的鎖管理也可能導(dǎo)致糟糕的響應(yīng)性丐巫。如果某個線程長時間占有一個鎖(或者正在對一個大容器進行迭代,并且對每個元素進行計算密集的處理)勺美,而其他想要訪問這個容器的線程就必須等待很長時間递胧。
10.3.3 活鎖
活鎖(Livelock)時另一種形式的活躍性問題,盡管不會阻塞線程赡茸,但也不能繼續(xù)執(zhí)行缎脾,因為線程不斷重復(fù)執(zhí)行相同的操作,而且總會失敗占卧。
活鎖通常發(fā)生在處理事務(wù)消息的應(yīng)用程序中:如果不能成功地處理某個消息遗菠,那么消息處理機制將回滾整個事務(wù)联喘,并將它重新放到隊列的開頭。如果消息處理其在處理某種特定類型的消息時存在錯誤并導(dǎo)致它失敗辙纬,那么每當這個消息從隊列中取出并傳遞到存在錯誤的處理器時耸袜,都會發(fā)生事務(wù)回滾。由于這條消息又被放回到隊列開頭牲平,因此處理器將被反復(fù)調(diào)用,并返回先溝通的結(jié)果(有時候也被稱為毒藥消息域滥,Poison Message)纵柿。
雖然處理信息的線程沒有阻塞,但也無法繼續(xù)執(zhí)行下去启绰。這種形式的活鎖通常時由過度的錯誤恢復(fù)代碼造成的昂儒,因為它錯誤將不可修復(fù)的錯誤作為可修復(fù)的錯誤。
當多個相互協(xié)作的線程都對彼此進行響應(yīng)從而修改各自的狀態(tài)委可,并使得任何一個線程都無法繼續(xù)執(zhí)行時渊跋,就發(fā)生了活鎖。
這就像兩個過于禮貌的人在半路上面對面相遇了:他們彼此都讓出對方的路着倾,然后又在另一條路上相遇了拾酝,因此他們就這樣反復(fù)地避讓下去。
要解決這種活鎖問題卡者,需要在重試機制中引入隨機性(randomness)蒿囤。
例如,在網(wǎng)絡(luò)上崇决,如果有兩臺機器嘗試使用相同的載波來發(fā)送數(shù)據(jù)包材诽,那么這些數(shù)據(jù)包就會發(fā)生沖突。這兩臺機器都檢查到了沖突恒傻,并都在稍后再次發(fā)送脸侥。
如果二者都選擇了在0.1秒后重試,那么會再次沖突盈厘,并且不斷沖突下去睁枕,因而即使有大量閑置的寬帶,也無法使數(shù)據(jù)包發(fā)送出去扑庞。
為了避免這種情況發(fā)生譬重,需要讓它們分別等待一段隨機的時間(以太協(xié)議定義了在重復(fù)發(fā)生沖突時采用指數(shù)方式回退機制,從而降低在多臺存在沖突的機器之間發(fā)生擁塞和反復(fù)失敗的風險)罐氨。
在并發(fā)應(yīng)用程序中臀规,通過等待隨機長度的時間和回退可以有效地避免活鎖的發(fā)生。