死锁

当两个或多个线程的某个组的每个成员必须等待其中一个成员执行某些操作(例如,释放锁定)才能继续之前发生死锁。没有干预,线程将永远等待。

容易出现死锁设计的伪代码示例如下:

thread_1 {
    acquire(A)
    ...
    acquire(B)
    ...
    release(A, B)
}

thread_2 {
    acquire(B)
    ...
    acquire(A)
    ...
    release(A, B)
}

thread_1 获得 A 时,可能会发生僵局,但还没有 B,而 thread_2 已经获得了 B,但不是 A。如下图所示,两个线程将永远等待。 死锁图

如何避免死锁

作为一般经验法则,最小化锁的使用,并最小化锁和解锁之间的代码。

以相同顺序获取锁

thread_2 的重新设计解决了这个问题:

thread_2 {
    acquire(A)
    ...
    acquire(B)
    ...
    release(A, B)
}

两个线程以相同的顺序获取资源,从而避免死锁。

此解决方案称为资源层次结构解决方案。Dijkstra 提议将其作为餐饮哲学家问题的解决方案。

有时即使你指定了锁定获取的严格顺序,也可以在运行时使这种静态锁定获取顺序动态化。

考虑以下代码:

void doCriticalTask(Object A, Object B){
     acquire(A){
        acquire(B){
            
        }
    }
}

这里即使锁获取顺序看起来是安全的,当 thread_1 访问此方法时也会导致死锁,例如,Object_1 作为参数 A,Object_2 作为参数 B,thread_2 按相反的顺序执行,即 Object_2 作为参数 A,Object_1 作为参数 B.

在这种情况下,最好使用 Object_1 和 Object_2 通过某种计算得到一些唯一条件,例如使用两个对象的哈希码,因此每当不同的线程以任何参数顺序进入该方法时,每次该唯一条件将导出锁定获取订单。

例如,Say Object 有一些唯一的密钥,例如 Account 对象的 accountNumber。

void doCriticalTask(Object A, Object B){
    int uniqueA = A.getAccntNumber();
    int uniqueB = B.getAccntNumber();
    if(uniqueA > uniqueB){
         acquire(B){
            acquire(A){
                
            }
        }
    }else {
         acquire(A){
            acquire(B){
                
            }
        }
    }
}