死锁问题

什么是死锁?

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
JavaEE-死锁的成因和解决方案.-LMLPHP
死锁就是两个或者多个相互竞争资源的线程, 你等我, 我等你, 你不放我也不放, 这就造成了他们之间的互相等待, 导致了 “永久” 阻塞.

死锁的四个必要条件

  • 互斥使用: 线程1拿到了锁, 线程2就得进入阻塞状态(锁的基本特性).
  • 不可抢占: 线程1拿到锁之后, 必须是线程1主动释放, 不可能线程1还没有释放, 线程2强行获取到锁.
  • 请求和保持: 线程1拿到锁A后, 再去获取锁B的时候, A这把锁仍然保持, 不会因为要获取锁B就把A释放了.
  • 循环等待: 线程1先获取锁A再获取锁B, 线程2先获取锁B再获取锁A, 线程1在获取锁B的时候等待线程2释放B,同时线程2在获取锁A的时候等待线程1释放A
  • 前三点 synchronized锁的基本特性, 我们是无法改变的, 循环等待是这四个条件里唯一 一个和代码结构相关的, 是我们可以控制的.

    常见的死锁及解决

    不可重入造成的死锁

    同一个线程针对同一个对象, 连续加锁两次, 如果锁不是可重入锁, 就会造成死锁问题.

    synchronized具有可重入性, 而在Java中还有一个ReentrantLock锁也是可重入锁, 所以说, 在Java程序中, 不会出现这种死锁问题.

    循环等待

    哲学家就餐问题(多个线程多把锁)

    JavaEE-死锁的成因和解决方案.-LMLPHP

    JavaEE-死锁的成因和解决方案.-LMLPHP
    分析可知每个哲学家有两种状态:

    1. 思考(相当于线程的阻塞状态)
    2. 拿起筷子吃面条(相当于线程获取到锁然后执行)

    我们可以假设一种极端的情况:同一时刻,所有的哲学家同时拿起右手的筷子,这时哲学家需要拿起左手的筷子才能吃饭,然而没有筷子可拿,都在等左边的哲学家放下筷子,这时这里的筷子就相当于程序中的锁,陷入了互相阻塞等待的状态,也就是典型的循环等待造成的死锁问题。

    解决方案:
    我们可以给按筷子编号, 哲学家们拿筷子时需要遵守一个规则, 拿筷子需要先拿编号小的, 再拿编号大的, 哲学家 2, 3, 4, 5 分别拿起了两手边编号为 1, 2, 3, 4 编号较小的筷子, 而1号哲学家想要拿到编号较小的1号筷子发现已经被拿走了, 此时就空出了5号筷子, 这样5号哲学家就可以拿起5号筷子去吃饭了, 等5号哲学家放下筷子后, 4号哲学家就可以拿起4号筷子去吃饭了, 以此类推…

    对应到程序中, 这样的做法其实就是在给锁编号, 然后再按照一个规定好的顺序来加锁, 任意线程加多把锁的时候, 都让线程遵守这个顺序, 这样就解决了互相阻塞等待的问题.

    两个线程两把锁

    两个线程两把锁, t1, t2线程先各自针对锁A, 锁B加锁, 然后再去获取对方的锁, 此时双方就会陷入僵持状态, 造成了死锁问题.

    观察下面的代码及执行结果:

    这里的代码是为了构造一个死锁的场景, 代码中的sleep是为了确保两个线程先把第一个锁拿到, 因为线程是抢占式执行的, 如果没有sleep的作用, 这里的死锁场景是不容易构造出来的.

    public class Test {
        public static void main(String[] args) {
            Object A = new Object();
            Object B = new Object();
            Thread t1 = new Thread(() -> {
                synchronized (A) {
                    System.out.println(Thread.currentThread().getName()+"获取到了锁A");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    synchronized (B) {
                        System.out.println(Thread.currentThread().getName()+"获取到了锁B");
                    }
                }
            }, "t1");
            Thread t2 = new Thread(() -> {
                synchronized (B) {
                    System.out.println(Thread.currentThread().getName()+"获取到了锁B");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    synchronized (A) {
                        System.out.println(Thread.currentThread().getName()+"获取到了锁A");
                    }
                }
            }, "t2");
            t1.start();
            t2.start();
        }
    }
    
    

    JavaEE-死锁的成因和解决方案.-LMLPHP
    t1线程获取到了锁A但并没有获取到锁B, t2线程获取到了锁B但并没有获取到锁A, 也就是说t1和t2两个线程进入了相互阻塞的状态, 线程无法获去到两把锁,。
    t1线程此时是处于BLOCKED状态的, 表示获取锁, 获取不到的阻塞状态;
    t2线程此时也是处于BLOCKED阻塞状态的;

    解决方案:
    们让t1和t2两个线程都按照相同的顺序来获取锁, 比如这里规定先获取锁A, 再获取锁B, 这样按照相同的顺序去获取锁就避免了循环等待造成的死锁问题, 代码如下:

    @SuppressWarnings({"all"})
    public class Test {
        public static void main(String[] args) {
            Object A = new Object();
            Object B = new Object();
            Thread t1 = new Thread(() -> {
                synchronized (A) {
                    System.out.println(Thread.currentThread().getName()+"获取到了锁A");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    synchronized (B) {
                        System.out.println(Thread.currentThread().getName()+"获取到了锁B");
                    }
                }
            }, "t1");
            Thread t2 = new Thread(() -> {
                synchronized (A) {
                    System.out.println(Thread.currentThread().getName()+"获取到了锁B");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    synchronized (B) {
                        System.out.println(Thread.currentThread().getName()+"获取到了锁A");
                    }
                }
            }, "t2");
            t1.start();
            t2.start();
        }
    }
    
    

    最后的执行结果两个线程都获取到了A,B锁.
    JavaEE-死锁的成因和解决方案.-LMLPHP

03-30 02:30