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来说,就相当于规定了哲学家拿取筷子的方式,比如只能拿哲学家左右两侧编号较小的筷子

五.总结

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

目录
相关文章
|
8月前
|
人工智能 Kubernetes Java
回归开源,两位 Java 和 Go 程序员分享的开源贡献指引
Higress是一个基于Istio和Envoy的云原生API网关,支持AI功能扩展。它通过Go/Rust/JS编写的Wasm插件提供可扩展架构,并包含Node和Java的console模块。Higress起源于阿里巴巴,解决了Tengine配置重载及gRPC/Dubbo负载均衡问题,现已成为阿里云API网关的基础。本文介绍Higress的基本架构、功能(如AI网关、API管理、Ingress流量网关等)、部署方式以及如何参与开源贡献。此外,还提供了有效的开源贡献指南和社区交流信息。
781 33
|
8月前
|
Java 程序员 应用服务中间件
【高薪程序员必看】万字长文拆解Java并发编程!(2 2-2)
📌 核心痛点暴击:1️⃣ 面了8家都被问synchronized锁升级?一张图看懂偏向锁→重量级锁全过程!2️⃣ 线程池参数不会配?高并发场景下这些参数调优救了项目组命!3️⃣ volatile双重检测单例模式到底安不安全?99%人踩过的内存可见性大坑!💡 独家亮点抢先看:✅ 图解JVM内存模型(JMM)三大特性,看完再也不怕指令重排序✅ 手撕ReentrantLock源码,AQS队列同步器实现原理大揭秘✅ 全网最细线程状态转换图(附6种状态转换触发条件表)
142 0
|
8月前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
257 0
|
8月前
|
设计模式 缓存 安全
【高薪程序员必看】万字长文拆解Java并发编程!(8):设计模式-享元模式设计指南
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的经典对象复用设计模式-享元模式,废话不多说让我们直接开始。
186 0
|
8月前
|
存储 安全 Java
【高薪程序员必看】万字长文拆解Java并发编程!(7):不可变类设计指南
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中Java不可变类设计指南,废话不多说让我们直接开始。
149 0
|
8月前
|
机器学习/深度学习 消息中间件 存储
【高薪程序员必看】万字长文拆解Java并发编程!(9-2):并发工具-线程池
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发编程中的强力并发工具-线程池,废话不多说让我们直接开始。
299 0
|
6月前
|
Arthas 监控 Java
Java死锁 如何定位?如何避免Java死锁?(图解+秒懂+史上最全)
Java死锁 如何定位?如何避免Java死锁?(图解+秒懂+史上最全)
Java死锁 如何定位?如何避免Java死锁?(图解+秒懂+史上最全)
|
8月前
|
存储 监控 算法
Java程序员必学:JVM架构完全解读
Java 虚拟机(JVM)是 Java 编程的核心,深入理解其架构对开发者意义重大。本文详细解读 JVM 架构,涵盖类加载器子系统、运行时数据区等核心组件,剖析类加载机制,包括加载阶段、双亲委派模型等内容。阐述内存管理原理,介绍垃圾回收算法与常见回收器,并结合案例讲解调优策略。还分享 JVM 性能瓶颈识别与调优方法,分析 Java 语言特性对性能的影响,给出数据结构选择、I/O 操作及并发同步处理的优化技巧,同时探讨 JVM 安全模型与错误处理机制,助力开发者提升编程能力与程序性能。
Java程序员必学:JVM架构完全解读
|
9月前
|
人工智能 Java 程序员
Java程序员在AI时代必会的技术:Spring AI
在AI时代,Java程序员需掌握Spring AI技术以提升竞争力。Spring AI是Spring框架在AI领域的延伸,支持自然语言处理、机器学习集成与自动化决策等场景。它简化开发流程,无缝集成Spring生态,并提供对多种AI服务(如OpenAI、阿里云通义千问)的支持。本文介绍Spring AI核心概念、应用场景及开发步骤,含代码示例,助你快速入门并构建智能化应用,把握AI时代的机遇。
1861 61
|
8月前
|
网络协议 Java 大数据
【高薪程序员必看】万字长文拆解Java并发编程!(1)
📌 核心痛点暴击:1️⃣ 面了8家都被问synchronized锁升级?一张图看懂偏向锁→重量级锁全过程!2️⃣ 线程池参数不会配?高并发场景下这些参数调优救了项目组命!3️⃣ volatile双重检测单例模式到底安不安全?99%人踩过的内存可见性大坑!💡 独家亮点抢先看:✅ 图解JVM内存模型(JMM)三大特性,看完再也不怕指令重排序✅ 手撕ReentrantLock源码,AQS队列同步器实现原理大揭秘✅ 全网最细线程状态转换图(附6种状态转换触发条件表)
138 0