当两个或多个线程互相等待时被阻塞,就会发生死锁。例如,第一个线程被第二个线程阻塞,它在等待第二个线程持有的一个资源。而第二个线程在获得第一个线程持有的某个资源之前不会释放这个资源。由于第一个线程在获得第二个线程持有的那个资源之前不会释放它自己所持有的资源,而第二个线程在获得第一个线程持有的一个资源之前也不会释放它所持有的资源,于是这两个线程就被死锁。
在编写多线程代码时,死锁是最难处理的问题之一。因为死锁可能在最意想不到的地方发生,所以查找和修正它既费时又费力。例如,试考虑下面这段锁定了多个对象的代码。
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 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 value = arr1[i] arr2[i]; } } } return value; } public void run() { //... } }
|
在第一个示例中,ArrayWithLockOrder
类是作为数组的一个包装提供的。每创建该类的一个新对象,该类就将 static num_locks
变量加 1。一个单独的 lock_order
实例变量被设置为 num_locks
static
变量的当前值。这可以保证,对于该类的每个对象,lock_order
变量都有一个独特的值。