Java多线程(八)

目录

一、产生死锁的情况

1.1 一个线程多把锁

1.1.1 Java中可重入锁的实现机制

1.2 两个线程两把锁

1.3 N个线程M把锁

二、解决死锁的方案

2.1 死锁的必要条件

2.2 破除循环等待


一、产生死锁的情况

死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

1.1 一个线程多把锁

在一个线程多把锁的情况下,首先成功地拿到第一个锁,但是当遇到第二个锁的时候就会产生阻塞等待,解决这个阻塞等待就需要先进行锁释放,而锁释放就需要线程进入。就好比是想打开房门,但是房门钥匙在房间里面。这个过程就产生了一直等待的结果变成死锁。

synchronized (locker) {
    synchronized (locker) {
        System.out.println("一个线程多把锁");
    }
}

但是在Java中这个问题是不会产生死锁的,因为synchronized是可重入锁,可以灵活的判定这个锁是不是已经加过了,从而不会产生死锁。但是在c++的锁就是不可重入锁,这种代码的写法就是会产生死锁。

1.1.1 Java中可重入锁的实现机制

由于Java中的synchronized锁是可重入锁,在一个线程多把锁的情况下也不会重复加锁,但是这就引出了另一个问题,在中间加锁的过程中锁是不应该被释放的,那jvm又如何知道什么时候去释放锁呢?

sychronized的解决方式是利用一个计数器,没当遇到一个加锁操作的时候就+1,遇到一个解锁操作的时候就-1,当计数器为0的时候才真正的去释放锁。

1.2 两个线程两把锁

两个线程分别获取一把锁,然后再去获取对方的锁,这样就会产生死锁。

public class Demo21 {
    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("对locker2对象加锁");
               }
           }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker2) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker1) {
                    System.out.println("对locker1对象加锁");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

这里程序运行出来就会产生死锁,当这里的locker1想要去获取locker2时,由于locker2已经加锁了就需要等待locker2释放锁,但是locker2释放锁又需要去获取locker1的锁,而locker1也是处于加锁的一个状态。就好比是回家发现房门钥匙在车里,然后车钥匙在家里。这样就产生了一直等待变成死锁。

1.3 N个线程M把锁

“哲学家就餐问题”,这里引入5个哲学家5根筷子来对应5个线程5把锁的情况。每个哲学家要就餐就需要先拿起左手筷子再拿右手的筷子,而其他的哲学家想就餐就只能等待其中的一根筷子放下。 

在一般的情况下是不会产生冲突的,但是当5个哲学家同时拿起左手的筷子时,就会产生一直等待的情况。这样就会产生死锁。

二、解决死锁的方案

2.1 死锁的必要条件

1.互斥使用:一个线程获取到这个锁,别的线程不能获取这个锁

2.不可抢占:锁只能由前一个持有者主动释放,不能被其他线程抢走

3.请求和保持:一个线程获取多把锁的时候,会对前一个锁保持获取状态

4.循环等待:要获取一个被持有的锁时,就需要一直等待,直到持有者释放锁

了解到了死锁的必要条件,只要破除其中一个就能够解决死锁问题。但是在前两个条件中,既是死锁的必要条件也是锁的特性,所以这两个条件无法破除。所以解决死锁问题只能从3和4下手,但是如果要解决3的条件就需要更改大部分代码的逻辑,成本是很高的。所以为了解决死锁问题,最好的解决办法就是破除循环等待的条件。

2.2 破除循环等待

银行家算法可以解决死锁问题,但是较为复杂,在解决死锁问题的过程中可能会引发更加复杂的问题,所以一般解决死锁问题,不建议使用银行家算法。

这里引入一种简洁有效的方法去解决死锁问题:针对锁进行编号,并且规定加锁顺序。例如,每个线程想要获取多把锁就需要先获取编号最小的锁。

同样的将两个线程两把锁的情况按照这种情况进行修改就会解决死锁问题

public class Demo21 {
    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("对locker2对象加锁");
               }
           }
        });
        Thread t2 = new Thread(()->{
            synchronized (locker1) {//先获取编号顺序小的锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2) {
                    System.out.println("对locker1对象加锁");
                }
            }
        });
        t1.start();
        t2.start();
    }
}

再次运行发现已经解决死锁问题了

猜你喜欢

转载自blog.csdn.net/x2656271356/article/details/132206146