1.一些程序是包含死锁风险的,但是并不会立即显示出来,一般死锁发生在高负载的情况下。
2,死锁类别:
a.)静态锁顺序死锁:如果两个线程试以不同的顺序获得相同的锁,就会产生锁顺序死锁。
如果每个线程都以固定的顺序来获得锁,那么就就不会出现锁顺序死锁。
例子: private final Objectleft = new Object();
private final Objectright = new Object();
public void leftRight(){
synchronized(left){
synchronized(right){
//dosomething();
}
}
}
public void rightLeft(){
synchronized(right){
synchronized(left){
//dosomething();
}
}
}
如果2个线程分别调用leftRight()和rightLeft(),则会产生锁顺序死锁,根本原因就不同线程获取锁的顺序的不同。
b.)动态锁顺序死锁,有的时候我们锁的顺序是模糊的,看下列例子。
public void transforMoney(ObjectfromAccount,ObjecttoAccount,int dollar){
synchronized (fromAccount) {
synchronized (toAccount) {
fromAccount.delete(dollar);
toAccount.add(dollar);
}
}
}
看以上程序,我们发现无论哪个地方使用了这个方法,锁的顺序”看起来”都是一致的,都是先锁fromAccount,再锁toAccount。
此时,就掉入陷阱了,因为你应该知道,方法的参数的顺序取决于方法的调用者,这里的fromAccount和toAccount只是参数定义,实际传入的参数顺序是不一定的,此时仍然会有锁顺序死锁问题。
如果有2个实际参数:youAccount和myAccount
A线程:transforMoney(youAccount,myAccount,20);
B线程:transforMoney(myAccount,youAccount,30);
这时只需要一个不恰当的执行时序,死锁就会发生。
解决办法:我们可以在方法内部通过一些参数的属性来定义锁的顺序。
比如使用System.identityHashCode方法,该方法将返回Object.hashCode()返回的值。例子如下:
private static final ObjectonlyOne= new Object();
public void transforMoney(ObjectfromAccount,Object toAccount,int dollar){
class Transfer{
public void transfer(){
fromAccount.delete(dollar);
toAccount.add(dollar);
}
}
int fromHash = System.identityHashCode(fromAccount);
int toHash = System.identityHashCode(toAccount);
if(fromHash <toHash){
synchronized(fromAccount){
synchronized (toAccount) {
new Transfer().transfer();
}
}
}else if(fromHash >toHash){
synchronized (toAccount) {
synchronized (fromAccount) {
new Transfer().transfer();
}
}
}else{
//唯一锁,同时只有1个线程能获得
synchronized (onlyOne) {
synchronized (toAccount) {
synchronized (fromAccount) {
new Transfer().transfer();
}
}
}
}
一些情况下是会产生哈希冲突,此时必须通过某种任意的方法来决定锁的顺序,而这可能又会重新引入死锁。
解决办法:此时加入一个唯一锁,也称加时锁(private static final 确定的唯一变量,类变量),在获得这两个锁之前,先要获得这个加时锁,从而保证每次只有1个线程以未知的顺序获得这两个锁。
如果时常发生哈西冲突,那么这种加时锁技术会成为并发瓶颈,因为这类似整个程序只有1个锁的情况。
当然我们也可以使用一些唯一的,不可变的属性来定义锁的顺序(例如ID,UUID之类的,可以通过一些属性来对锁进行排序,然后自己定义锁的规则)。
c.)在协作对象之间的锁顺序死锁(通常是因为在持有锁的情况下调用某个外部方法):
协作对象之间的锁获取操作不太明显。看例子:
class A{
private int a,b;
private final BobjectB = new B();
public synchronized void A(int a){
this.a = a;
if(a == b){
objectB.setSomething(b);
}
}
public synchronized void setSomething(int b){
b = a;
}
}
class B{
private int c,d;
private final AobjectA = new A();
public synchronized void setSomething(int d){
d = c;
}
public synchronized void B(int b){
this.c = b;
if(c == d){
objectA.setSomething(c);
}
}
}
在A中的A()方法中,我们要先获取this锁,由于调用了B对象的setSomething方法,因为setSomething方法是同步方法,所以必须获得获得setSomething方法需要的锁,也就是B锁。
也就是需要先获得A锁再获得B锁,在B中的B方法也一样,需要先获得B锁再获得A锁。
如果2个线程同时调用A()和B(),将发生锁顺序死锁。
这类的锁顺序死锁问题主要因为在持有锁时调用了某个外部方法。
结论:如果在持有锁的时候调用某个外部方法,那么将出现活跃性问题。在这个外部方法中可能会获取其他锁(获取其他锁导致锁的顺序不一定,可能产生死锁),或者阻塞时间过长,导致其他线程无法及时获得当前被持有的锁。
d.)开放调用:如果在调用某个方法的时候不需要持有锁,那么这种调用被称为开放调用,在程序中应尽量使用开放调用,与那些在持有锁时调用外部方法的程序相比,更容易对依赖于开放调用的程序进行死锁分析。
实现方式:不要直接使方法直接成为synchronized的,而是使同步代码块仅仅保护那些涉及共享状态的操作(说白了就是缩小代码块)。
e.)资源死锁:当在相同的资源集合上等待时,也会发生死锁,此时称它为资源死锁。
比如线程A连接了D1资源,需要D2资源。
线程B连接了D2资源,需要D1资源,此时就会产生资源死锁。
线程饥饿死锁也是一种资源死锁,一个任务等待其他任务的结果,那么这些任务往往是产生线程饥饿死锁的来源。
有界线程池/资源池与相互依赖的任务不能一起使用。
3.死锁的解决:a.)不使用内置锁,使用Lock类中的定时tryLock锁来代替内置锁,显示锁可以指定一个超时时限,在等待超时的时候返回一个失败信息。自己再制定恢复或者中断机制。
b.)通过线程转储来分析死锁:线程转储包含各个运行中县城的栈追踪信息,还包含加锁信息,例如:每个线程持有了那些锁,在哪些栈帧中获得这些锁,以及被阻塞的线程中正在等待获取哪一个锁。
线程转储在内置锁和显示锁上的区别:java6包含对显示锁的线程转储和死锁检测等的支持,但在这些锁上获得的信息比在内置锁上的信息精确度低。内置锁与获得它们所在的线程栈帧是相关联的,而显示的Lock只与它的线程相关联。
4.饥饿:引发饥饿的最常见资源就是”CPU时钟周期”
a.)当线程由于无法访问它所需要的资源而不能继续执行时,就发生了”饥饿”问题。
b.)线程优先级使用不当也会产生饥饿。
c.)在持有锁的时候执行了一些无法结束的结构(例如无限循环或者无限等待某个资源),其他需要这个锁的线程无法得到这个锁,也会产生饥饿问题。
饥饿的关键字是:资源,锁资源,cpu资源等等等。
注意:应该尽量避免使用线程优先级,这会增加平台依赖性,并可能导致活跃性问题。
5.糟糕的响应性:
GUI框架中,如果你的后台任务是cpu密集型的,会与主的事件线程竞争cpu的时钟周期,可能导致cpu主线程的响应性,这时可以降低后台线程的优先级。
不良的锁管理也会导致糟糕响应性。
6.活锁:当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行的时候,就发生了活锁。
要解决活锁问题,要引入随机性,不要都采用相同的响应机制(比如都等待相同的时间等)。