线程死锁
死锁是指两个或多个线程被阻塞,等待获得死锁中的其他线程所持有的锁。当多个线程同时需要相同的锁,但以不同的顺序获得它们时,就会发生死锁。
例如,如果线程1锁定了A并试图锁定B,而线程2已经锁定了B并试图锁定A,那么就会出现死锁。线程1永远不会得到B,线程2永远不会得到a。此外,它们都不会知道。它们将永远阻塞各自的对象A和B。这种情况就是死锁。
Thread1 lock A,wait B
Thread2 lock B,wait A
实例代码:
TreeNode parent = null;
List children = new ArrayList();
public synchronized void addChild(TreeNode child){
if(!this.children.contains(child)) {
this.children.add(child);
child.setParentOnly(this);
}
}
public synchronized void addChildOnly(TreeNode child){
if(!this.children.contains(child){
this.children.add(child);
}
}
public synchronized void setParent(TreeNode parent){
this.parent = parent;
parent.addChildOnly(this);
}
public synchronized void setParentOnly(TreeNode parent){
this.parent = parent;
}
如果一个线程A在另一个父线程B调用child.setParent(parent)方法的同时调用parent.addChild(child)方法,则在相同的父实例和子实例上,可能会发生死锁.
注意:如上所述,这两个线程必须同时调用parent. addchild (child)和child. setparent (parent),并且在相同的两个父实例和子实例上才会发生死锁。
线程确实需要同时获取锁。例如,如果线程1比线程2早一点,因此锁定了a和B,那么线程2在尝试锁定B时就已经被阻塞了,这样就不会发生死锁。由于线程调度通常是不可预测的,所以无法预测何时发生死锁。
数据库死锁
可能发生死锁的更复杂的情况是数据库事务。一个数据库事务可能包含许多SQL更新请求。当一个记录在一个事务期间被更新时,该记录将被锁定以接受其他事务的更新,直到第一个事务完成为止。因此,同一事务中的每个更新请求可能会锁定数据库中的一些记录。
如果多个事务同时运行,需要更新相同的记录,则有可能导致死锁。
代码描述:
Transaction 1, request 1, locks record 1 for update
Transaction 2, request 1, locks record 2 for update
Transaction 1, request 2, tries to lock record 2 for update.
Transaction 2, request 2, tries to lock record 1 for update.
死锁预防
在某些情况下,可以防止死锁。
给锁排序
当多个线程需要相同的锁,但获取它们的顺序不同时,就会发生死锁。如果您确保任何线程总是以相同的顺序获取所有锁,那么就不会发生死锁。
Thread 1:
lock A
lock B
Thread 2:
wait for A
lock C (when A locked)
Thread 3:
wait for A
wait for B
wait for C
如果一个线程(比如线程3)需要多个锁,它必须按照确定的顺序获取这些锁。在获得之前的锁之前,它不能获得序列中后面的锁。
锁排序是一种简单而有效的死锁预防机制。但是,只有在您知道需要使用的所有锁之后才能使用它。
设置锁超时
在锁定尝试上设置超时,这意味着试图获得锁定的线程在放弃之前只会尝试这么长时间。如果线程在给定的超时时间内没有成功获取所有必要的锁,它将备份、释放所有获取的锁,随机等待一段时间,然后重试。等待的随机时间为尝试使用相同锁的其他线程提供了获取所有锁的机会,从而让应用程序在没有锁定的情况下继续运行。
锁定超时机制的一个问题是,在Java中不可能为进入同步块设置超时。您必须创建一个自定义锁类或者使用并发包里面的工具类。
死锁检测
死锁检测是一种更重的死锁预防机制,针对无法进行锁排序和锁定超时的情况。
每当一个线程获得一个锁时,它就会在线程和锁的数据结构(map, graph等)中被记录下来。此外,每当线程请求一个锁时,这个数据结构也会注意到。
当线程请求锁但请求被拒绝时,该线程可以遍历锁图以检查死锁。例如,如果线程a请求锁7,但是锁7被线程B持有,那么线程a可以检查线程B是否请求了线程a持有的任何锁(如果有的话)。如果线程B请求这样做,就会发生死锁(线程a获取锁1,请求锁7,线程B获取锁7,请求锁1)。
如果检测到死锁,线程会做什么呢?
一个可能的操作是释放所有锁、备份、随机等待一段时间,然后重试。这类似于更简单的锁超时机制,只是线程只在实际发生死锁时进行备份。不仅仅是因为它们的锁请求超时了。但是,如果许多线程竞争相同的锁,即使它们备份并等待,它们也可能会重复地以死锁告终。
更好的选择是确定或分配线程的优先级,以便只备份一个(或几个)线程。其余线程继续获取它们需要的锁,就像没有发生死锁一样。如果分配给线程的优先级是固定的,那么相同的线程将总是被给予更高的优先级。为了避免这种情况,可以在检测到死锁时随机分配优先级。