當兩個或多個線程互相等待時被阻塞,就會發生死鎖。例如,第一個線程被第二個線程阻塞,它在等待第二個線程持有的一個資源。而第二個線程在獲得第一個線程持有的某個資源之前不會釋放這個資源。由於第一個線程在獲得第二個線程持有的那個資源之前不會釋放它自己所持有的資源,而第二個線程在獲得第一個線程持有的一個資源之前也不會釋放它所持有的資源,於是這兩個線程就被死鎖。
在編寫多線程代碼時,死鎖是最難處理的問題之一。因為死鎖可能在最意想不到的地方發生,所以查找和修正它既費時又費力。例如,試考慮下面這段鎖定了多個對象的代碼。
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;
}
這段代碼在求和操作中訪問兩個數組對象之前正確地鎖定了這兩個數組對象。它形式簡短,編寫也適合所要執行的任務;但不幸的是,它有一個潛在的問題。這個問題就是它埋下了死鎖的種子,除非您在不同的線程中對相同的對象調用該方法時格外小心。要查看潛在的死鎖,請考慮如下的事件序列:
sumArrays(ArrayA, ArrayB);
sumArrays(ArrayB, ArrayA);
避免這種問題的一種方法是讓代碼按固定的全局順序獲取鎖。在本例中,如果線程 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 域重新開始的可能性,則您在建立鎖定對象的預定義順序時應該仔細斟酌。