當兩個或多個線程互相等待時被阻塞,就會發生死結。例如,第一個線程被第二個線程阻塞,它在等待第二個線程持有的一個資源。而第二個線程在獲得第一個線程持有的某個資源之前不會釋放這個資源。由於第一個線程在獲得第二個線程持有的那個資源之前不會釋放它自己所持有的資源,而第二個線程在獲得第一個線程持有的一個資源之前也不會釋放它所持有的資源,於是這兩個線程就被死結。
在編寫多線程代碼時,死結是最難處理的問題之一。因為死結可能在最意想不到的地方發生,所以尋找和修正它既費時又費力。例如,試考慮下面這段鎖定了多個對象的代碼。
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;
}
這段代碼在求和操作中訪問兩個數組對象之前正確地鎖定了這兩個數組對象。它形式簡短,編寫也適合所要執行的任務;但不幸的是,它有一個潛在的問題。這個問題就是它埋下了死結的種子,除非您在不同的線程中對相同的對象調用該方法時格外小心。要查看潛在的死結,請考慮如下的事件序列:
- 建立兩個數組對象,
ArrayA 和 ArrayB。
- 線程 1 用下面的調用來調用
sumArrays 方法:
sumArrays(ArrayA, ArrayB);
- 線程 2 用下面的調用來調用
sumArrays 方法:
sumArrays(ArrayB, ArrayA);
- 線程 1 開始執行
sumArrays 方法並在 //1 處獲得對參數 a1 的鎖,對於這個調用而言,它就是對 ArrayA 對象的鎖。
- 然後在 //2 處,線上程 1 獲得對
ArrayB 的鎖之前被搶先。
- 線程 2 開始執行
sumArrays 方法並在 //1 處獲得對參數 a1 的鎖,對於這個調用而言,它就是對 ArrayB 對象的鎖。
- 然後線程 2 在 //2 處試圖擷取對參數 a2 的鎖,它是對
ArrayA 對象的鎖。因為這個鎖當前由線程 1 持有,所以線程 2 被阻塞。
- 線程 1 開始執行並在 //2 處試圖擷取對參數 a2 的鎖,它是對
ArrayB 對象的鎖。因為這個鎖當前由線程 2 持有,所以線程 1 被阻塞。
- 現在兩個線程都被死結。
避免這種問題的一種方法是讓代碼按固定的全域順序擷取鎖。在本例中,如果線程 1 和線程 2 按相同的順序對參數調用 sumArrays 方法,就不會發生死結。但是,這一技術要求,多線程代碼的程式員在調用那些鎖定作為參數傳入的對象的方法時需要格外小心。在您遇到這種死結並不得不進行調試之前,使用這一技術的應用程式似乎不切實際。
另外,您也可以將鎖定順序內嵌物件的內部。這允許代碼查詢它準備為其獲得鎖的對象,以確定正確的鎖定順序。只要即將鎖定的所有對象都支援鎖定順序標記法,並且擷取鎖的代碼遵循這一策略,就可避免這種潛在死結的情況。
在對象中嵌入鎖定順序的缺點是,這種實現將使記憶體需求和運行時成本增加。另外,在上例中應用這一技術需要在數組中有一個封裝對象,用來存放鎖定順序資訊。例如,試考慮下面的代碼,它由前面的樣本修改而來,其中實現了鎖定順序技術:
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++; // 鎖數加 1。
lock_order = num_locks; // 為此對象執行個體設定唯一的 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; // 保留數組引用的一個
ArrayWithLockOrder last = a2; // 本機複本。
int size = a1.array().length;
if (size == a2.array().length)
{
if (a1.lockOrder() > a2.lockOrder()) // 確定並設定對象的鎖定
{ // 順序。
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 類是作為數組的一個封裝提供的。每建立該類的一個新對象,該類就將 static num_locks 變數加 1。一個單獨的 lock_order 執行個體變數被設定為 num_locks static 變數的當前值。這可以保證,對於該類的每個對象,lock_order 變數都有一個獨特的值。lock_order 執行個體變數充當此對象相對於該類的其他對象的鎖定順序指標。
請注意,static num_locks 變數是在 synchronized 語句中進行操作的。這是必須的,因為對象的每個執行個體共用該對象的 static 變數。因此,當兩個線程同時建立 ArrayWithLockOrder 類的一個對象時,如果操作 static num_locks 變數的代碼未作同步處理,該變數就可能被破壞。對此代碼作同步處理可以保證,對於 ArrayWithLockOrder 類的每個對象,lock_order 變數都有一個獨特的值。
此外還更新了 sumArrays 方法,以使它包括確定正確鎖定順序的代碼。在請求鎖之前,將查詢每個對象以獲得它的鎖定順序。編號較小的首先被鎖定。此代碼可以保證,不管各對象是以什麼順序傳給此方法,它們總是被以相同的順序鎖定。
static num_locks 域和 lock_order 域都是作為 long 類型實現的。long 資料類型是作為 64 位元有符號二進位補碼整數實現的。這意味著在建立 9,223,372,036,854,775,807 個對象之後,num_locks 和 lock_order 的值將重新開始。您未必會達到這個極限,但在適當的條件下這是可能發生的。
實現嵌入的鎖定順序需要投入更多的工作,使用更多的記憶體,並會延長執行時間。但是,如果您的代碼中可能存在這些類型的死結,您也許會發現值得這樣做。如果您無法承受額外的記憶體和執行開銷,或者不能接受 num_locks 或 lock_order 域重新開始的可能性,則您在建立鎖定對象的預定義順序時應該仔細斟酌。