• 关于我对线程安全问题中死锁的理解


    1.什么是死锁


    比如张三谈了一个女朋友,张三就对这个女朋友加锁了。
    此时李四也看上了这个女生,但是他只能等待张三分手(解锁)后,才能和这个女生谈恋爱。

    李四为了等待这个女生,错过了好多喜欢他的人,这里就相当于线程无法执行的后续工作,
    此时就相当于是死锁了。

    一旦程序出现死锁,就会导致线程无法执行后序工作了,此时程序必然会有严重的 bug 。
    发生死锁的概率又是随机的,因此死锁是非常隐蔽,不容易被发现的。

    2.三个典型情况

    情况1:

    一个线程如果有一把锁,连续加锁两次。如果这个锁是不可重入锁,就会死锁。

    java 中的 synchronizedReentrantLock 都是可重入锁,
    因此这一种情况演示不了。


    情况2:

    两个线程两把锁,t1 和 t2 各自先针对 锁1 和 锁2 加锁,之后再尝试获取对方的锁。

    比如说,张三的车钥匙锁在屋里了,而屋子的钥匙锁在车了。
    这个时候屋子和车都进不去了,就会产生问题。

    下面来举例说明。

    package thread;
    
    public class ThreadDemo16 {
        public static void main(String[] args) {
            Object locker1 = new Object();
            Object locker2 = new Object();
    
            Thread t1 = new Thread(() -> {
                synchronized (locker1) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    synchronized (locker2) {
                        System.out.println("线程t1拿到两个锁");
                    }
                }
            });
    
            Thread t2 = new Thread(() -> {
                synchronized (locker2) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (locker1) {
                        System.out.println("线程t2拿到两个锁");
                    }
                }
            });
            t1.start();
            t2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37


    这里并没有输出结果,说明线程并没有拿到两把锁。

    这个时候可以使用 jconsole 来查看当前的进程的情况。


    按照这样的路径查找 jconsole ,然后双击。



    看到这样的窗口,双击选中的进程。






    红色框框的表示 获取锁获取不到的阻塞状态
    绿色框框的表示 发生错误的代码行数






    情况3: 多个线程,多把锁。(向较与情况2的一半情况

    例子:哲学家就餐问题



    每个哲学家有两种状态:

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

    由于操作系统的随机调度,这五个哲学家,随时都可能想吃面条,也随时可能要思考人生。

    如果想吃面条就需要拿起左手和右手的筷子。


    假如同一时刻,所有的哲学家都拿起左手的筷子吃面条。
    此时如果要成功吃到面条,就要等到右边的哲学家放下手中的筷子,自己才可以吃到面条。
    此时就会死锁!!!

    只有一只筷子没有办法吃面条,必须要等到右边的老铁放下筷子才可以吃。
    如果右边一直不放,左边的老铁就一直吃不到。

    3.可重入与不可重入

    一个线程针对同一个对象,如果不会发生问题就叫可重入的,否则就叫不可重入的。

    class Counter {
        public int count = 0;
    
        synchronized public void add() {
            synchronized(this) {
                count++;
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    锁对象是 this ,只要有线程调用 add 方法,进入 add 方法的时候,
    在可以加锁成功的前提下,就会先加锁。紧接着又遇到了代码块,此时会再次尝试加锁。

    站在 锁对象的视角(this),他认为自己已经被其他的线程给占用了,
    那么这里的第二次加锁是不需要阻塞等待的。

    如果允许上述操作,这个锁就是可重入的;不允许就是不可重入的。
    如果是不可重入的,就会发生 死锁。

    上面演示的就是不可重入的死锁。


    下面演示的是可重入的思索。

    java 为了避免不小心出现闭锁现象,就把 synchronized 给设置成可重入的了。
    因此 java 中才会无法演示上面的情况1.

    4.死锁的四个必要条件

    1、互斥使用 — 线程1拿到了锁,线程1就需要等待着。

    2、不可抢占 — 线程1拿到锁之后,如果线程1不释放锁,线程2就不能强行获取。

    3、请求和等待 — 线程1获取到锁A之后,再去获取到锁B,
    此时锁A还会继续被线程1获取。(不会因为获取锁B后就把锁A给释放了)

    4、循环等待 — 线程1尝试获取到锁A锁B,线程2尝试获取到锁B锁A
    线程1在获取B的时候等待线程2释放B,同时线程2在获取A的时候等待线程1释放A

    5.如何破除死锁

    打破循环等待这个必要条件。

    解决办法:

    给每个筷子编号,指定固定的顺序(从小到大)拿筷子。


    上图是规定从小到大的拿。


    到最后一个老铁拿的时候,会拿一号筷子。
    但是这个一号筷子被其他的老铁拿了,此时这个老铁就发生阻塞等待了。
    此时拿四号筷子的老铁会把五号筷子也拿了,之后开始吃面条。


    这个老铁吃面条的时候,拿三号筷子的老铁就会看着他吃。
    等待这个老铁吃完,放下两支筷子号筷子,三号筷子的老铁就可以拿起四号筷子来吃了。


    按照这样的方式,所有的老铁都可以吃面条。


    下面由代码来演示:

    package thread;
    
    public class ThreadDemo16 {
        public static void main(String[] args) {
            Object locker1 = new Object();
            Object locker2 = new Object();
    
            //给两个锁编号:1 、2,规定locker1是1号,locker2是2号,按照从小到大的顺序拿
            Thread t1 = new Thread(() -> {
                //先拿序号小的
                synchronized (locker1) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    //后拿序号大的
                    synchronized (locker2) {
                        System.out.println("线程t1拿到两个锁");
                    }
                }
            });
    
            Thread t2 = new Thread(() -> {
                synchronized (locker1) {
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (locker2) {
                        System.out.println("线程t2拿到两个锁");
                    }
                }
            });
            t1.start();
            t2.start();
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41



    这种方法是解决死锁,最简单最可靠的方法。

  • 相关阅读:
    专精特新新企业技术创新发展趋势研究分析讲座详情
    已备案域名用国外服务器会不会掉备案?
    ios ipa包上传需要什么工具
    【ML】第二章 端到端机器学习项目
    2023前端面试题总结
    nfs+rpcbind实现服务器之间的文件共享
    开发人员的首选:CodeWhisperer
    bootstrapTable jqGrid使用总结
    末端物流自动配送车规模之战,毫末智行如何赢下三个赛点?
    Git下载安装及基本配置
  • 原文地址:https://blog.csdn.net/m0_63033419/article/details/128127419