死锁产生的必要条件:
互斥条件:一个资源只能被一个一个进程使用
占有且等待:一个进程因请求资源而被阻塞时,对已经获得的资源保持占有状态;
不可强行占有:进程已经获得的资源不会被其他进程抢占;
循环等待:若干进程间产生一种循环等待的条件,比如A等待B释放资源,B等待A释放资源。
避免死锁的方法:
给锁加上顺序
确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。
这种方式需要你事先知道所有可能会用到的锁,但总有些时候是无法预知的。
加锁时限
给锁加上一个等待的时长,当等待超时,就会放弃对该资源的请求,同时释放自己已经获得的资源,等待一段时间在重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行。
此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多。
这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。
java.util.concurrent
死锁检测
死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。
每当一个线程获得了锁,就在线程和锁的数据结构中将其记下。同时,每当有线程请求锁,也记录在其中。
当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。
那么当检测出死锁时,这些线程该做些什么呢?
一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。
一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。