造成线程死锁的原因以及解决方案
造成死锁的原因
一个线程一把锁
这种情况 可重入锁没事,不可重入锁会造成死锁, Java 中 synchronized是可重入锁, 因此没有问题
(所谓重入锁,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。)
public class Test {
static Object locker1 = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
test();
});
t1.start();
t1.join();
System.out.println("没有造成死锁!");
}
public static void test() {
// 多个锁情况
synchronized (locker1) {
synchronized (locker1){
System.out.println("执行测试方法");
}
}
}
}
运行结果如下: 使用synchronized 并没有造成死锁情况 但是如果不是synchronized的话,是其他的不可重入锁 就会造成死锁情况
两个线程两把锁
两个线程两把锁, 此时当两个线程获取两把锁对象的时候顺序并不一样,此时很容易造成死锁
public class Test {
static Object locker1 = new Object();
static Object locker2 = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
test();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread t2 = new Thread(() -> {
test2();
});
t2.start();
}
public static void test() throws InterruptedException {
// 多个锁情况
synchronized (locker1) {
System.out.println("t1 进入第一把锁!");
// 为了让线程 2拿到 锁2
Thread.sleep(1000);
synchronized (locker2){
System.out.println("t1 进入第二把锁!");
}
}
}
public static void test2() {
synchronized (locker2) {
System.out.println("t2 进入第一把锁! ");
synchronized (locker1) {
System.out.println("t2 进入第二把锁! ");
}
}
}
}
运行结果如下:
线程1和线程2都进入到了第一把锁里面 但是谁都没有释放锁 ,因此导致了线程的死锁情况
其实解决方案很简单, 只需要指定加锁顺序,(外层锁1, 内层锁2) 这样即使 线程1 拿到了外层锁1 , 此时线程 2 拿不到 锁1 就进不去, 就会阻塞等待,直到线程1执行完程序, 释放锁1 和锁2 此时线程2 才能拿到锁1 执行下去.
public class Test {
static Object locker1 = new Object();
static Object locker2 = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
try {
test();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t1.start();
Thread t2 = new Thread(() -> {
test2();
});
t2.start();
t1.join();
t2.join();
System.out.println("没有造成死锁!");
}
// 指定两个方法都是外层锁1加锁, 内层锁2 加锁.
public static void test() throws InterruptedException {
// 多个锁情况
synchronized (locker1) {
System.out.println("t1 进入第一把锁!");
Thread.sleep(1000);
synchronized (locker2){
System.out.println("t1 进入第二把锁!");
}
}
}
public static void test2() {
synchronized (locker1) {
System.out.println("t2 进入第一把锁! ");
synchronized (locker2) {
System.out.println("t2 进入第二把锁! ");
}
}
}
}
结果如下:
n个线程m把锁
举个例子, 就餐问题,如果有五个人(5个线程) 此时筷子也只有五根(5把锁) 但是每个人都需要拿两根筷子才可以就餐,(一个线程调用某个方法需要抢两把锁) 此时如果每个人都拿到了面前的一根筷子,那么谁都吃不了饭, 也就是说, 线程进入了死锁状态!
因此总结得出 ,死锁的四个必要条件:
- 因为线程是抢占式执行,并且锁是互斥使用, 因此一个线程拿到一把锁的时候, 另一个线程就不能使用 (锁的基本特点)
- 不可占用: 一个线程拿到了锁,只能够自己执行完方法 ,释放锁资源, 而不能被其他线程强行占有
- 请求和保持, 在一个方法内,如果线程拿到一把锁之后,还会想着抢第二把锁,(代码向下执行的特点) 因此,第一把锁在没有执行完该方法,或者在synchronized加锁范围内 是不会释放锁的,并且不断请求获取第二把锁, 从而形成死锁
- 循环等待, 相当于加锁顺序不同, “家钥匙锁在车里了, 车钥匙放家里了”.
死锁的解决方案
因此针对与形成死锁的四个必要条件,总结出解决方案:
针对锁进行编号, 如果需要同时获取多把锁的情况,约定加锁顺序, 务必是先对小的编号加锁,后对大的编号加锁
例如 N个线程 M把锁情况:
约定好先获取编号小的筷子, 在获取编号大的筷子 ,并且五个人每个人需要两根筷子,满足只允许4个人同时拿起筷子,那么就不会造成死锁 , 因此此时肯定有一个人能拿到两根筷子,当这个人吃完的时候, 就可以同时释放两个资源,那么就不会造成死锁现象的发生.
例如 两个线程两把锁的情况:
只需要约定好加锁顺序,不构造成线程1拿到锁 1, 线程2拿得到锁2 的情况即可.
每个人需要两根筷子,满足只允许4个人同时拿起筷子,那么就不会造成死锁 , 因此此时肯定有一个人能拿到两根筷子,当这个人吃完的时候, 就可以同时释放两个资源,那么就不会造成死锁现象的发生.
例如 两个线程两把锁的情况:
只需要约定好加锁顺序,不构造成线程1拿到锁 1, 线程2拿得到锁2 的情况即可.