一.为什么会产生死锁
1.锁是互斥的
一个线程拿到锁后,另一个线程想要再尝试获取锁,必然要阻塞等待。
2.锁是不可抢占的(不可剥夺)
一个线程拿到锁后,另一个线程想要再尝试获取锁,必然要阻塞等待,而不是把锁抢过来。
3.请求和保持
一个获取锁1后,不释放锁1的情况下,获取锁2
4.循环等待
多个线程多把锁的情况下,多个线程多把锁之间构成循环。例如:A等待B,B等待C,C等待A。
二.产生死锁的三种场景
1.一个线程一把锁
代码示例:
public static void main(String[] args) {
Object locker1 = new Object();
Thread t = new Thread(() -> {
synchronized (locker1) {
synchronized (locker1) {
System.out.println("......");
}
}
});
}
第一次加锁能够成功,但第二次加锁时锁已经被占用,只能进行阻塞等待,而第一次加锁又没有走完因此无法释放,导致第二次加锁会一直阻塞等待。程序无法进行。
但synchronized关键字为了避免以上情况,具有可重入特性,无论写多少层只计入第一层。
如何自己实现一个可重入锁?
1.在锁内部记录是哪个线程持有锁,后续每次加锁进行判定
2.通过计数器,记录当前加锁的次数,从而确定何时真正解锁
2.两个线程两把锁
两个线程两把锁,每个线程获取到一把锁后,尝试获取对方的锁。
代码示例:
package Thread;
public class Demo2 {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2) {
System.out.println("t1线程获取到两把锁");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker1) {
System.out.println("t2线程获取到两把锁");
}
}
});
}
}
sleep()函数是为了确保每个线程已经执行到获取第二把锁的阶段,如果不加,可能会出现某个线程一次性获取到两把锁,顺利执行的情况,不能达成死锁。
3.多个线程多把锁
同上,多个线程多把锁之间构成循环。例如:A等待B,B等待C,C等待A。
三.如何避免死锁
1.避免锁嵌套(破坏请求和保持)
把嵌套的锁改成并列的锁,使得不会出现一个线程持有一个锁并等待另一个线程持有的锁的情况。
package Thread;
public class Demo2 {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
synchronized (locker2) {
System.out.println("t1线程获取到两把锁");
}
});
Thread t2 = new Thread(() -> {
synchronized (locker2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
synchronized (locker1) {
System.out.println("t2线程获取到两把锁");
}
});
}
}
2.约定加锁顺序(破坏循环等待)
使得线程在请求锁时不会形成一个封闭的循环等待链,从而破坏了死锁产生的 “循环等待条件”,有效地避免了死锁的发生。
例如:
package Thread;
public class Demo2 {
public static void main(String[] args) {
Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (locker1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2) {
System.out.println("t1线程获取到两把锁");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (locker1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (locker2) {
System.out.println("t2线程获取到两把锁");
}
}
});
}
}