JAVA应用程序无法从死锁中恢复,如果遇到这种情况我们就只能终止并重启它,并寄希望它不会再次出现。因此在设计的时候一定要排除那些可能导致死锁出现的条件。
死锁不是每次都出现,但往往实在最糟糕的情况--高负载的时候出现。呵呵
死锁
对于死锁要注意由多个线程形成的环路的锁依赖关系,很容易导致死锁。
锁顺序死锁
当2个线程试图以不同的顺序调用相同的锁,就有可能发生顺序死锁。如:
A--->锁住left--->尝试锁住right--->永久等待
B------>锁住right--->尝试锁住left--->永久等待
从上我们可见,如果都已相同的顺序获取锁的话就不会发生上述情况。由此可得,如果所有线程以固定顺序来获得锁,那么在程序中就不会出现锁顺序死锁问题。
动态的锁顺序死锁
有时候我们并不清楚在锁顺序上有足够的控制权来避免死锁的发生。如
public void transforMoney(Account fromAccount,Account toAccount,DollarAmount amount){
synchronized(fromAccount){
synchronized(toAccount){
……
}
}
}
为了避免这种情况,我们需要控制加锁的顺序,但是参数顺序是无法控制的,所以我们自能从Account自身来处理。
1、通过比较Account来进行加锁,如首先加载小的,或是存在唯一性的东西
2、如果无法比较,可以通过System.identityHashCode,返回对象的hashCode,通过比较hashCode来加锁。当然存在极端的情况就是得到的hashCode值相同,这就需要考虑别的方法了,这种极端情况极少发生,其实可以放心使用
3、最后一种方法就是使用加时赛锁,算是在hashCode基础上再增加区别条件
在协作对象之间发生的死锁
在持有锁时调用某个外部方法,那么将出现活跃性问题。在这个外部方法中可能会获取其他锁,或者阻塞时间过长,导致其他线程无法及时获得当前被持有的锁。
如,汽车位置的实时显示。我们在设置汽车的位置时需要加锁,而显式汽车位置时也需要加锁,放置重复调用。这是2个不同的代码,它们都用到了汽车的位置这个公共资源,如果在设置位置的同时显示汽车位置,那么就会发生活跃性问题。当然实际情况中我们可以使用并发容器来实现。
开放调用
如果在调用某个方法时不需要持有锁,那么这种调用就被称为开放调用。在程序中应尽可能使用开发调用。与那些在持有锁时调用外部方法的程序相比,更易于对依赖于开放调用的程序进行死锁分析。
开放调用类似于封装技术,将需要同步的东西封装在程序内部,对外这部分不可见,也就不会对外产生影响,就避免了协作对象间的死锁问题。
资源死锁
这个顾名思义,就是对资源的争夺且不释放导致的死锁啦。
死锁的避免和诊断
如果一个程序每次至多只能获得一个锁,那么就不会产生锁顺序死锁。如果必须使用多个锁,那么在设计的时候必须考虑锁的顺序;尽量减少潜在的加锁交互数量,将好哦去锁时需要遵循的协议写入正式文档并始终尊续这些协议。
在使用细粒度锁的程序中,可以通过以下2个阶段来检查代码中的死锁:
1、找出在什么地方将获取多个锁,这个集合要尽量的小
2、对所有这些实例进行全局分析,从而确保它们在整个程序中获取锁的顺序都保持一致。
支持定时的锁
有一项技术可以检测死锁和从死锁中恢复,就是使用显式锁Lock的定时功能,如果超出指定时限,就会退出锁。这个很接近于哲学家就餐问题的解决方案了,即在无法获取所有资源的时候,释放手中的资源,等待一段时间再去获取。
通过线程转储信息来分析死锁
我们可以通过JVM的线程转储来识别死锁的发生
其他活跃性危险
饥饿
当线程由于无法访问它所需的资源而不能继续执行时,就发生了饥饿。引发饥饿的最常见资源就是CPU时钟周期。
糟糕的响应性
这种情况常见于糟糕的GUI应用程序。如果调用了一个耗时的后台线程,那么前台就会失去响应。可以使用SwingWork来避免这种情况。
活锁
活锁不会阻塞线程,但是不能继续执行。 当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。就好比在路上两个相向的人,互相让路,每次都移动到同一边,一直反复的避让下去,就发生了活锁。