Skip to content

Latest commit

 

History

History
100 lines (64 loc) · 5.67 KB

死锁.md

File metadata and controls

100 lines (64 loc) · 5.67 KB

死锁

这里开门见山直接给出死锁的概念:死锁的一个比较专业的定义是:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象

下面通过一个典型的例子来阐释死锁的现象

我们假设在以前,银行的转账操作还没有现在那么发达,大部分的业务操作都是要人为参与的。
所以我们可以想象每一个用户在银行都有一个账本,用来记录每一次资金的操作,现在假设有个
客户王五要到银行找柜员A做个转账业务:向张三转入100元,此时张三也找另外一个柜员B做转
账业务:向王五转账100元,此时柜员A和B都会去拿账本进行操作有一种可能,柜员A拿到了王五
的账本,柜员B拿到了张三的账本,因为转账操作需要两个账本进行操作,所以柜员A拿到王五的
账本后就等着拿张三的账本,同样柜员B拿到了张三的账本后就等着拿王五的账本,这里要完成转
账操作需要两个账本操作,但现在这种情况怎么办,这就是我们所谓的死锁的场景。

如何预防死锁

并发程序一旦死锁,一般没有特别好的方法,很多时候我们只能重启应用。因此,解决死锁问题最好的办法还是规避死锁,这里就要引出了出现死锁的四个条件,其定义是这四个条件同时出现才会发生死锁,反过来分析,也就是说只要我们破坏其中一个,就可以成功避免死锁的发生。

死锁的4个条件

  • 互斥,共享资源 X 和 Y 只能被一个线程占用;
  • 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
  • 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
  • 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。

注意 一般互斥这个条件我们没有办法破坏

理论解决方法

  1. 对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。
  2. 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了
  3. 对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了

破坏占用且等待条件

这个概念可以结合前面的故事来进行处理,上面的转账的例子中就是柜员分别持有一本账本,然后等待另外一本账本,但此时就会出现死等的状态。所以我们结合设计思想中没什么是加一层中间件解决不了的,如果有 那就再加一层。同理这里我们可以设置一个账本管理员,并且只有管理员才能操作账本,例如,如果柜员A需要申请王五和张三的账本,此时如果账本管理员发现现在只有王五的账本,这个时候账本管理员不 会将单个账本给柜员,只有两本都在的时候才会给。这样就保证了“一次性申请所有资源。

破坏不可抢占条件

破坏不可抢占条件看上去很简单,核心是要能够主动释放它占有的资源,这一点synchronized 是做不到的。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资 源。java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的

破坏循环等待条件

破坏这个条件,需要对资源进行排序,然后按序申请资源。这个实现非常简单,我们假设每个账户都有不同的属性 id,这个 id 可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请。

如何检测死锁

使用jmap,jstack等命令来查看JVM的线程堆和栈,如果有死锁jstack 的输出中通常会有 Found one Java-level deadlock:的字样。 另外,实际项目中还可以搭配使用top、df、free等命令查看操作系统的基本情况,出现死锁可能会导致 CPU、内存等资源消耗过高。 采用 VisualVM、JConsole 等工具进行排查。

具体操作

  1. 运行code4j中的Deadlock的代码运行结果如下

img.png

  1. 打开命令行输入jps -l

img_1.png

  1. 打开JDK安装的目录下的bin包中找到jconsole.exe,双击运行,输入进程号,如下图

img_2.png img_3.png img_4.png

  1. 或者在命令行中输入jstack -l 进程号

img_5.png

解决死锁的银行家算法

这篇博客写的就很好,给出了大部分的参考资料和例题解释 https://blog.csdn.net/Q1743822183/article/details/139881674

修改之后的代码

new Thread(() -> {
        synchronized (resource1) {
            System.out.println(Thread.currentThread() + "get resource1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread() + "waiting get resource2");
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
            }
        }
    }, "线程 2").start();