當(dāng)兩個或多個線程互相等待時被阻塞,就會發(fā)生死鎖。例如,第一個線程被第二個線程阻塞,它在等待第二個線程持有的一個資源。而第二個線程在獲得第一個線程持有的某個資源之前不會釋放這個資源。由于第一個線程在獲得第二個線程持有的那個資源之前不會釋放它自己所持有的資源,而第二個線程在獲得第一個線程持有的一個資源之前也不會釋放它所持有的資源,于是這兩個線程就被死鎖。
在編寫多線程代碼時,死鎖是最難處理的問題之一。因?yàn)樗梨i可能在最意想不到的地方發(fā)生,所以查找和修正它既費(fèi)時又費(fèi)力。例如,試考慮下面這段鎖定了多個對象的代碼。
public int sumArrays(int[] a1, int[] a2)
{
int value = 0;
int size = a1.length;
if (size == a2.length) {
synchronized(a1) { //1
synchronized(a2) { //2
for (int i=0; i<size; i++)
value += a1[i] + a2[i];
}
}
}
return value;
}
這段代碼在求和操作中訪問兩個數(shù)組對象之前正確地鎖定了這兩個數(shù)組對象。它形式簡短,編寫也適合所要執(zhí)行的任務(wù);但不幸的是,它有一個潛在的問題。這個問題就是它埋下了死鎖的種子,除非您在不同的線程中對相同的對象調(diào)用該方法時格外小心。要查看潛在的死鎖,請考慮如下的事件序列:
創(chuàng)建兩個數(shù)組對象,ArrayA 和 ArrayB。
線程 1 用下面的調(diào)用來調(diào)用 sumArrays 方法:
sumArrays(ArrayA, ArrayB);
線程 2 用下面的調(diào)用來調(diào)用 sumArrays 方法:
sumArrays(ArrayB, ArrayA);
線程 1 開始執(zhí)行 sumArrays 方法并在 //1 處獲得對參數(shù) a1 的鎖,對于這個調(diào)用而言,它就是對 ArrayA 對象的鎖。
然后在 //2 處,在線程 1 獲得對 ArrayB 的鎖之前被搶先。
線程 2 開始執(zhí)行 sumArrays 方法并在 //1 處獲得對參數(shù) a1 的鎖,對于這個調(diào)用而言,它就是對 ArrayB 對象的鎖。
然后線程 2 在 //2 處試圖獲取對參數(shù) a2 的鎖,它是對 ArrayA 對象的鎖。因?yàn)檫@個鎖當(dāng)前由線程 1 持有,所以線程 2 被阻塞。
線程 1 開始執(zhí)行并在 //2 處試圖獲取對參數(shù) a2 的鎖,它是對 ArrayB 對象的鎖。因?yàn)檫@個鎖當(dāng)前由線程 2 持有,所以線程 1 被阻塞。
現(xiàn)在兩個線程都被死鎖。
避免這種問題的一種方法是讓代碼按固定的全局順序獲取鎖。在本例中,假如線程 1 和線程 2 按相同的順序?qū)?shù)調(diào)用 sumArrays 方法,就不會發(fā)生死鎖。但是,這一技術(shù)要求,多線程代碼的程序員在調(diào)用那些鎖定作為參數(shù)傳入的對象的方法時需要格外小心。在您碰到這種死鎖并不得不進(jìn)行調(diào)試之前,使用這一技術(shù)的應(yīng)用程序似乎不切實(shí)際。
另外,您也可以將鎖定順序嵌入對象的內(nèi)部。這答應(yīng)代碼查詢它預(yù)備為其獲得鎖的對象,以確定正確的鎖定順序。只要即將鎖定的所有對象都支持鎖定順序表示法,并且獲取鎖的代碼遵循這一策略,就可避免這種潛在死鎖的情況。
在對象中嵌入鎖定順序的缺點(diǎn)是,這種實(shí)現(xiàn)將使內(nèi)存需求和運(yùn)行時成本增加。另外,在上例中應(yīng)用這一技術(shù)需要在數(shù)組中有一個包裝對象,用來存放鎖定順序信息。例如,試考慮下面的代碼,它由前面的示例修改而來,其中實(shí)現(xiàn)了鎖定順序技術(shù):
class ArrayWithLockOrder
{
PRivate static long num_locks = 0;
private long lock_order;
private int[] arr;
public ArrayWithLockOrder(int[] a)
{
arr = a;
synchronized(ArrayWithLockOrder.class) {
num_locks++; // 鎖數(shù)加 1。
lock_order = num_locks; // 為此對象實(shí)例設(shè)置唯一的 lock_order。
}
}
public long lockOrder()
{
return lock_order;
}
public int[] array()
{
return arr;
}
}
class SomeClass implements Runnable
{
public int sumArrays(ArrayWithLockOrder a1,
ArrayWithLockOrder a2)
{
int value = 0;
ArrayWithLockOrder first = a1; // 保留數(shù)組引用的一個
ArrayWithLockOrder last = a2; // 本地副本。
int size = a1.array().length;
if (size == a2.array().length)
{
if (a1.lockOrder() > a2.lockOrder()) // 確定并設(shè)置對象的鎖定
{ // 順序。
first = a2;
last = a1;
}
synchronized(first) { // 按正確的順序鎖定對象。
synchronized(last) {
int[] arr1 == a1.array();
int[] arr2 == a2.array();
for (int i=0; i<size; i++)
value += arr1[i] + arr2[i];
}
}
}
return value;
}
public void run() {
//...
}
}
在第一個示例中,ArrayWithLockOrder 類是作為數(shù)組的一個包裝提供的。每創(chuàng)建該類的一個新對象,該類就將 static num_locks 變量加 1。一個單獨(dú)的 lock_order 實(shí)例變量被設(shè)置為 num_locks static 變量的當(dāng)前值。這可以保證,對于該類的每個對象,lock_order 變量都有一個獨(dú)特的值。lock_order 實(shí)例變量充當(dāng)此對象相對于該類的其他對象的鎖定順序指示器。
請注重,static num_locks 變量是在 synchronized 語句中進(jìn)行操作的。這是必須的,因?yàn)閷ο蟮拿總€實(shí)例共享該對象的 static 變量。因此,當(dāng)兩個線程同時創(chuàng)建 ArrayWithLockOrder 類的一個對象時,假如操作 static num_locks 變量的代碼未作同步處理,該變量就可能被破壞。對此代碼作同步處理可以保證,對于 ArrayWithLockOrder 類的每個對象,lock_order 變量都有一個獨(dú)特的值。
此外還更新了 sumArrays 方法,以使它包括確定正確鎖定順序的代碼。在請求鎖之前,將查詢每個對象以獲得它的鎖定順序。編號較小的首先被鎖定。此代碼可以保證,不管各對象是以什么順序傳給此方法,它們總是被以相同的順序鎖定。
static num_locks 域和 lock_order 域都是作為 long 類型實(shí)現(xiàn)的。long 數(shù)據(jù)類型是作為 64 位有符號二進(jìn)制補(bǔ)碼整數(shù)實(shí)現(xiàn)的。這意味著在創(chuàng)建 9,223,372,036,854,775,807 個對象之后,num_locks 和 lock_order 的值將重新開始。您未必會達(dá)到這個極限,但在適當(dāng)?shù)臈l件下這是可能發(fā)生的。
實(shí)現(xiàn)嵌入的鎖定順序需要投入更多的工作,使用更多的內(nèi)存,并會延長執(zhí)行時間。但是,假如您的代碼中可能存在這些類型的死鎖,您也許會發(fā)現(xiàn)值得這樣做。假如您無法承受額外的內(nèi)存和執(zhí)行開銷,或者不能接受 num_locks 或 lock_order 域重新開始的可能性,則您在建立鎖定對象的預(yù)定義順序時應(yīng)該仔細(xì)斟酌。
新聞熱點(diǎn)
疑難解答
圖片精選