死锁
导致死锁的原因
Java中死锁最简单的情况是,一个线程T1持有锁L1并且申请获得锁L2,而另一个线程T2持有锁L2并且申请获得锁L1,因为默认的锁申请操作都是阻塞的,所以线程T1和T2永远被阻塞了。导致了死锁。
这是最容易理解也是最简单的死锁的形式。但是实际环境中的死锁往往比这个复杂的多。
可能会有多个线程形成了一个死锁的环路,比如:线程T1持有锁L1并且申请获得锁L2,而线程T2持有锁L2并且申请获得锁L3,而线程T3持有锁L3并且申请获得锁L1,这样导致了一个锁依赖的环路:T1依赖T2的锁L2,T2依赖T3的锁L3,而T3依赖T1的锁L1。从而导致了死锁。
从这两个例子,我们可以得出结论,产生死锁可能性的最根本原因是:线程在获得一个锁L1的情况下再去申请另外一个锁L2,也就是锁L1想要包含了锁L2,也就是说在获得了锁L1,并且没有释放锁L1的情况下,又去申请获得锁L2,这个是产生死锁的最根本原因。另一个原因是默认的锁申请操作是阻塞的。
一旦我们在一个同步方法中,或者说在一个锁的保护的范围中,调用了其它对象的方法时,就要十而分的小心:
1)如果其它对象的这个方法会消耗比较长的时间,那么就会导致锁被我们持有了很长的时间;
2)如果其它对象的这个方法是一个同步方法,那么就要注意避免发生死锁的可能性了;
最好是能够避免在一个同步方法中调用其它对象的延时方法和同步方法。如果不能避免,就要采取上面说到的编码技巧,打破死锁的闭环,防止死锁的发生。同时我们还可以尽量使用“不可变对象”来避免锁的使用,在某些情况下还可以避免对象的共享,比如 new 一个新的对象代替共享的对象,因为锁一般是对象上的,对象不相同了,也就可以避免死锁,另外尽量避免使用静态同步方法,因为静态同步相当于全局锁。还有一些封闭技术可以使用:比如堆栈封闭,线程封闭,ThreadLocal,这些技术可以减少对象的共享,也就减少了死锁的可能性。
总结一下:
死锁的根本原因:
1)是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致了一个锁依赖的闭环;
2)默认的锁申请操作是阻塞的。所以要避免死锁,就要在一遇到多个对象锁交叉的情况,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性。要采取各种方法来杜绝这种可能性。
3)如果有同步代码块的嵌套,就会容易出现死锁问题,以后要注意!
一个简单的死锁例子
思路是创建两个对象 obj_A 和 obj_B,再创建两个线程 t1 和 t2,让每个线程都用synchronized
锁住字符串( t1 先锁obj_A,再去锁 obj_B ;t2 先锁 obj_B,再锁 obj_A),如果 t1 锁住obj_A,t2 锁住 obj_B,t1 就没办法锁住 obj_B,t2也没办法锁住 obj_A,这时就陷入了死锁。直接贴代码:
演示代码如下:
1.MyThread.java:
public class MyThread extends Thread {
public static Object obj_A = new Object();
public static Object obj_B = new Object();
public boolean flag;
public MyThread(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if (flag) {
synchronized (obj_A) {
System.out.println("这是if的A锁");
synchronized (obj_B) {
System.out.println("这是if的B锁");
}
}
} else {
synchronized (obj_B) {
System.out.println("这是else的B锁");
synchronized (obj_A) {
System.out.println("这是else的A锁");
}
}
}
}
}
2.Test.java:
public class Test {
public static void main(String[] args) {
MyThread t1 = new MyThread(true);
MyThread t2 = new MyThread(false);
t1.start();
t2.start();
}
}
理想中的输出应该是:四句话都可以打印出来,但是绝大部分情况会产生死锁,也就是如上面所说的,只能打印出来两句话,这个例子就会发生死锁。
所以:如果有同步代码块的嵌套,就会容易出现死锁问题,以后要注意!