【并发编程】死锁

来源:互联网 发布:js怎么设置display 编辑:程序博客网 时间:2024/06/05 11:09

死锁是两个或更多线程阻塞着等待其它处于处于死锁状态的线程所持有的锁。简单说,就是线程之间互相得不到说需要的锁。

死锁通常发生在多个线程同时但以不同的顺序请求同一组锁的时候。

例如:如果线程1锁住了A,然后尝试对B进行加锁,同时线程2已经锁住了B,接着尝试对A进行加锁,这时死锁就发生了。线程1永远得不到B,线程2也永远得不到A,他们将永远的阻塞下去。这就是死锁。

public class TreeNode {    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;    }}

如果线程1调用parent.addChild(child)方法的同时有另外一个线程2调用child.setParent()方法,两个线程中的parent表示的是同一个对象,child亦然,此时就会发生死锁。

Thread 1:parent.addChild(child); //locks parent

   --> child.setParentOnly(parent);

Thread 2:child.setParent(parent); //locks child

   -->parent.addChildOnly();

首先线程1调用parent.addChild(child)。因为addChild()是同步的,所以线程1会对parent对象加锁,其他线程则不能访问。

然后线程2调用child.setParent(parent)。因为setParent()是同步的,所以线程2会对child对象加锁,其它线程则不能访问。

现在child和parent被两个不同的线程锁住了。接下来线程1尝试调用child.setParentOnly()方法,但是由于child对象现在被线程2锁住了,所以该调用会被阻塞。线程2也尝试调用parent.addChildOnly(),但是由于parent对象现在被线程1锁住了,导致线程2也阻塞在该方法处。导致的结果就是,两个线程都被阻塞并等待着获取对方所持有的锁,这就是死锁。永远都得不到。

还有另外一种死锁的情况,

Thread 1 locks A, wait for B

Thread 2 locks B, wait for C

Thread 3 locks C, wait for D

Thread 4 locks D, wait for A


如何避免死锁

1.加锁顺序:由上已经知道了,导致死锁的原因就是线程之间互相锁住了对方需要的资源,那么说明,当多个线程需要相同的一些锁,如果加锁的顺序不同,就容易发生死锁。那么解决办法就是确保所有线程都是按照相同的顺序获取锁。

2.加锁时限:尝试获取锁的时候加一个超时时间,这就意味着在尝试获取锁的过程中若超出了这个时间,该线程则放弃对该锁的请求。若一个线程没有在给定的时间内成功获得说需要的锁,则会进行回退并释放所有已获得的锁,然后等待一段时间再试。(加锁超时后可以先干其它事情,然后再回头重复之前加锁的逻辑。)

这种机制存在一个问题,超时和重试的机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会非常大,因此就算因为竞争而超时后,由于超时的时间一样,就会产生新的竞争,带来新的问题。并且,在java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁。

3.死锁检测:

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。

当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。


那么当检测出死锁时,这些线程该做些什么呢?

一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。

一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。





原创粉丝点击