multithreading 死锁

示例

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

容易死锁的设计的伪代码示例为:

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){
            
        }
    }
}

在这里,即使锁获取顺序看起来很安全,当线程_1使用对象_1作为参数A和对象_2作为参数B并且线程_2以相反的顺序(即对象_2作为参数A和对象_1作为参数B)访问此方法时,也可能导致死锁。

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

例如,说对象具有一些唯一的键,例如,对于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){
                
            }
        }
    }
}