死锁的必要条件:
- 互斥:一份资源每次只能被一个进程或线程使用(在Java中一般体现为,一个对象锁只能被一个线程持有)
- 请求和保持:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已经被其他进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不释放。
- 不剥夺:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 环路等待:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
在java中最简单的死锁情况:
一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。
另外一个原因是默认锁的申请操作是阻塞的
要尽量避免在一个对象的同步方法里面调用其他对象的同步方法或者延时方法。
减小锁的范围,只获对需要的资源加锁,我们锁定了完整的对象资源,但是如果我们只需要其中一个字段,那么我们应该只锁定那个特定的字段而不是完整的对象。
如果两个线程使用 thread join 无限期互相等待也会造成死锁,我们可以设定等待的最大时间来避免这种情况。
死锁的调优:
jstack命令生成线程快照的原因主要是为了定位线程出现长时间停顿的原因,比如线程死锁,死循环,请求外部资源(数据库连接,网络资源,设备资源)导致的长时间等待
这里我们模拟一个死锁的小例子(请求资源的顺序不一样)
public class DeadLock {
static Object a=new Object();
static Object b=new Object();
public static void main(String[] args) {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
DeadLock.call_a_first();
}
});
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
DeadLock.call_b_first();
}
});
t1.start();
t2.start();
}
private static void call_a_first() {
synchronized (a) {
try {
System.out.println("锁足a等待b");
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (b) {
}
}
}
private static void call_b_first() {
synchronized (b) {
try {
System.out.println("锁足b等待a");
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized (a) {
}
}
}
}
输出:
锁足a等待b 锁足b等待a |