当前位置: 首页 > 图灵资讯 > 技术篇> 以全局的固定顺序获取多个锁来避免死锁

以全局的固定顺序获取多个锁来避免死锁

来源:图灵教育
时间:2024-02-25 14:02:13

当两个或两个以上的线程相互等待时,就会发生死锁。例如,第一个线程被第二个线程阻塞,它正在等待第二个线程持有的资源。在获得第一个线程持有的资源之前,第二个线程不会释放资源。因为第一线程在获得第二线程持有的资源之前不会释放自己的资源,而第二线程在获得第一线程持有的资源之前也不会释放自己的资源,所以这两个线程被锁定。

在编写多线程代码时,死锁是最难处理的问题之一。因为死锁可能发生在最意想不到的地方,所以找到和纠正它既费时又费力。例如,试着考虑下面一段锁定多个对象的代码。

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 处,在线程 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 对象之后,num_locks 和 lock_order 值将重新开始。你可能达不到这个极限,但在适当的条件下是可能的。

要实现嵌入式锁定顺序,需要投入更多的工作,使用更多的内存,并延长执行时间。但是,如果这些类型的死锁可能存在于你的代码中,你可能会发现值得这样做。如果你不能承担额外的内存和执行费用,或者你不能接受 num_locks 或 lock_order 在建立锁定对象的预定义顺序时,应仔细考虑域重新开始的可能性。