Java程序员-你真的了解死锁吗

简介: Java程序员-你真的了解死锁吗

Java程序员-你真的了解死锁吗

💕"i need your breath"💕

作者:Mylvzi

文章主要内容:死锁的成因和必要条件

一.什么是死锁

死锁:就是多个线程/进程因为相互等待而使得各自持有的资源无法继续执行,这就叫做死锁。

我们以可重入锁为例,引入今天要学习的死锁问题

二.可重入锁

1.概念

可重入锁指的是:一个线程针对同一把锁连续加锁两次,而不死锁,就说这个锁具有可重入性;反之,则不具有可重入性

synchronnized(locker) {
  // 对locker对象再次加锁
  synchronized(locker){}
  }

如果此时我们的锁不具有可重入性,则这个代码就会出现死锁的情况

2.Java的synchronized具有可重入性

被synchronized修饰的对象具有可重入性,也就是一个线程能够针对同一个对象连续加锁两次,而不发生死锁

之所以不会发生死锁 ,是因为锁会记录是被哪一个线程持有的,在下一次有线程尝试对该所进行加锁时,先判断该线程是否是持有该锁的线程,如果是,直接加锁成功,这样就避免了死锁的出现

3.释放锁的时机

synchronnized(locker) {
  // 对locker对象再次加锁
  synchronized(locker){
  }// 2处
  // ......未执行完的代码
  }

对于上述代码,在执行到2处的时候会unlock一次,但是需要释放整个锁吗?或者说能释放整个锁吗?答案显然是不能,因为该线程实际上还在持有这个锁,该线程还没有完全执行完毕,如果在2处直接释放锁,则其他线程就可以对该对象进行修改访问,出现线程安全问题,所以需要等到整个代码执行完毕才能释放锁

也就是说对于上述嵌套加锁的代码来说,无论加锁多少次,必须等到最外围结束才能释放锁

三.死锁常见的几种情况

1.一个线程 一把锁

一个线程连续对同一把锁加锁两次,如果是不可重入的,就会发生死锁(synchronized不会发生)

2.两个线程 两把锁

现在有两个线程tA,tB,还有两把锁locker1和lcoker2,先让tA对locker1进行加锁,tB对locker2加锁,保证两个线程各自持有一把锁;然后再让tA对locker2加锁,tB对locker1加锁,由于locker1和locker2都还没有被释放,且释放的条件是两个线程都被执行完毕,而要想执行完毕就必须对新的对象进行加锁,由于这种矛盾,导致两个线程都会一直处于阻塞状态,发生死锁

// 定义两个用于加锁的对象
private static Object locker1 = new Object();
private static Object locker2 = new Object();
// 创建两个线程
Thread t1 = new Thread(() -> {
  // 线程1对locker1进行加锁
  synchronized(locker1) {
    try {
                // sleep非常关键  必须先让两个线程都拥有一把锁,再去交叉加锁  这样才会发生死锁
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
    }
  // 线程1对locker2进行加锁
  synchronized(locker2) {
    System.out.println("t1 加锁locker2成功!!!")
})
Thread t2 = new Thread(() -> {
  // 线程2对locker2进行加锁
  synchronized(locker2) {
    try {
                // sleep非常关键  必须先让两个线程都拥有一把锁,再去交叉加锁  这样才会发生死锁
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
    }
  // 线程2对locker1进行加锁
  synchronized(locker1) {
    System.out.println("t2 加锁locker1成功!!!")
})

打印结果:

此时就发生了死锁,两个线程因为相互等待对方持有锁的释放而都处于阻塞状态,导致各自代码无法继续运行,产生死锁!!!

3.N个线程,M把锁

N个线程,M把锁的情况可以看作情况2的一个扩展,而对于这种情况的解释有一个很好的例子–哲学家就餐问题

现在有5个哲学家围在一个餐桌旁边,但是餐桌上只有5根筷子(不是5双筷子),只有当一个哲学家拿到两根筷子的时候他才能就餐,对于每一个哲学家来说,他只干两件事:

  1. 思考人生
  2. 就餐

就餐规则如下:

  1. 哲学家什么时候思考人生,什么时候就餐是随机的
  2. 哲学家如果正在就餐,其结束就餐的时机是随机的
  3. 当哲学家思考人生的时候,不会拿取一根筷子
  4. 当哲学家就餐的时候,会拿起左右两侧的两根筷子

其写到这里大家其实也能发现,这里的哲学家就是线程,筷子就是

在一般情况下,每个哲学家就餐持有两个筷子的时候,如果其他哲学家也需要其中的一个筷子,则其他的哲学家就要阻塞等待,等待上一个哲学家就餐完毕,他才能使用,但是如果有那么一瞬间,每个哲学家同时都拿起了自己左侧的筷子,此时他们还无法就餐,因为他们还需要右侧的筷子,当他们去寻找右侧的筷子的时候,发现其他哲学家已经拿走了,那他就要等待持有右侧筷子的哲学家就餐完毕,但是每个哲学家因为都只有一根筷子,就都无法就餐完毕,导致每个哲学家都在循环等待,每个哲学家都处于阻塞状态,发生了死锁

综上,死锁其实就是一种bug,死锁的存在会导致线程的崩溃,资源的的浪费,那如何解决死锁呢?要想解决死锁,先要了解死锁形成的四个必要条件

四.死锁形成的四个必要条件

必要条件是指四个条件都满足,才会发生死锁,死锁形成的四个必要条件大致可以分为两部分

  1. 锁的基本特性
  2. 代码结构导致死锁出现

下面来看死锁形成的四个必要条件:

  1. 互斥使用(锁的基本特性):当一个线程已经拥有一个锁时,另一个线程也想要持有这个锁,就要阻塞等待
  2. 不可强占(锁的基本特性):当一个锁已经被一个线程持有时,其他线程无法强制强占该锁(总不能我正吃着饭你把我筷子抢走吧,那我吃啥!)
  3. 请求保持(代码结构):当一个线程已经持有一个锁的时候,尝试去持有另一个锁(tA已经持有locker1了,还想持有locker2)这是典型的吃着碗里的,看着锅里的
  4. 循环等待(代码结构):每一个线程都在等待其他线程结束来使自己持有锁,等待形成了依赖关系(就是上面的哲学家就餐问题)

只有当上述四个条件都满足的时候才会发生死锁,所以说要想形成死锁,其实也是一件不简单的事~

那如何避免死锁呢?对于必要条件1,2来说,这是锁的基本特性,是由synchronized决定的,你无法更改,可以更改的条件只有3,4

对于3来说,我们可以尝试避免嵌套结构的出现,但是有些业务场景之下我们必须要进行嵌套的调用又该怎么办?其实,有一个方法可以很好的解决3,4这两种场景,即规定锁的使用顺序

比如对于3来说,我们规定每个线程获取锁的顺序是固定的,只能先获取locker1,再获取locker2,不能直接获取locker2,这样就避免了一个线程在持有锁的前提下再去持有其他锁

对于4来说,就相当于规定了哲学家拿取筷子的方式,比如只能拿哲学家左右两侧编号较小的筷子

五.总结

本文主要讲述了死锁的成因,由可重入锁引入什么是死锁,再讲述了死锁形成的三种常见情况,最后又讲述了死锁形成的四个必要条件–互斥使用,不可强占,请求等待,循环等待,欢迎大家补充,交流

目录
相关文章
|
3月前
|
Java 程序员
JAVA程序员的进阶之路:掌握URL与URLConnection,轻松玩转网络资源!
在Java编程中,网络资源的获取与处理至关重要。本文介绍了如何使用URL与URLConnection高效、准确地获取网络资源。首先,通过`java.net.URL`类定位网络资源;其次,利用`URLConnection`类实现资源的读取与写入。文章还提供了最佳实践,包括异常处理、连接池、超时设置和请求头与响应头的合理配置,帮助Java程序员提升技能,应对复杂网络编程场景。
94 9
|
4天前
|
人工智能 Java 程序员
【AI程序员】通义灵码 AI 程序员全面上线JAVA使用体验
通过 AI 程序编写一个JAVA后台项目登陆页面
133 17
|
6月前
|
存储 算法 Java
惊!Java程序员必看:JVM调优揭秘,堆溢出、栈溢出如何巧妙化解?
【8月更文挑战第29天】在Java领域,JVM是代码运行的基础,但需适当调优以发挥最佳性能。本文探讨了JVM中常见的堆溢出和栈溢出问题及其解决方法。堆溢出发生在堆空间不足时,可通过增加堆空间、优化代码及释放对象解决;栈溢出则因递归调用过深或线程过多引起,调整栈大小、优化算法和使用线程池可有效应对。通过合理配置和调优JVM,可确保Java应用稳定高效运行。
177 4
|
6月前
|
算法 Java 程序员
在Java的编程世界里,多态不仅仅是一种代码层面的技术,它是思想的碰撞,是程序员对现实世界复杂性的抽象映射,是对软件设计哲学的深刻领悟。
在Java的编程世界里,多态不仅仅是一种代码层面的技术,它是思想的碰撞,是程序员对现实世界复杂性的抽象映射,是对软件设计哲学的深刻领悟。
92 9
|
6月前
|
Java 程序员
Java数据类型:为什么程序员都爱它?
Java数据类型:为什么程序员都爱它?
66 1
|
3月前
|
SQL 存储 Java
面向 Java 程序员的 SQLite 替代品
SQLite 是轻量级数据库,适用于小微型应用,但其对外部数据源支持较弱、无存储过程等问题影响了开发效率。esProc SPL 是一个纯 Java 开发的免费开源工具,支持标准 JDBC 接口,提供丰富的数据源访问、强大的流程控制和高效的数据处理能力,尤其适合 Java 和安卓开发。SPL 代码简洁易懂,支持热切换,可大幅提高开发效率。
|
3月前
|
SQL Java 程序员
倍增 Java 程序员的开发效率
应用计算困境:Java 作为主流开发语言,在数据处理方面存在复杂度高的问题,而 SQL 虽然简洁但受限于数据库架构。SPL(Structured Process Language)是一种纯 Java 开发的数据处理语言,结合了 Java 的架构灵活性和 SQL 的简洁性。SPL 提供简洁的语法、完善的计算能力、高效的 IDE、大数据支持、与 Java 应用无缝集成以及开放性和热切换特性,能够大幅提升开发效率和性能。
|
4月前
|
IDE Java 程序员
C++ 程序员的 Java 指南
一个 C++ 程序员自己总结的 Java 学习中应该注意的点。
40 5
|
4月前
|
Java 大数据 程序员
我的程序员之路:自学Java篇
我的程序员之路:自学Java篇
|
6月前
|
监控 算法 安全
Java并发编程案例分析:死锁的检测与解决
Java并发编程案例分析:死锁的检测与解决
53 2