爆款云主机2核4G限时秒杀,88元/年起!
查看详情

活动

天翼云最新优惠活动,涵盖免费试用,产品折扣等,助您降本增效!
热门活动
  • 618智算钜惠季 爆款云主机2核4G限时秒杀,88元/年起!
  • 免费体验DeepSeek,上天翼云息壤 NEW 新老用户均可免费体验2500万Tokens,限时两周
  • 云上钜惠 HOT 爆款云主机全场特惠,更有万元锦鲤券等你来领!
  • 算力套餐 HOT 让算力触手可及
  • 天翼云脑AOne NEW 连接、保护、办公,All-in-One!
  • 中小企业应用上云专场 产品组合下单即享折上9折起,助力企业快速上云
  • 息壤高校钜惠活动 NEW 天翼云息壤杯高校AI大赛,数款产品享受线上订购超值特惠
  • 天翼云电脑专场 HOT 移动办公新选择,爆款4核8G畅享1年3.5折起,快来抢购!
  • 天翼云奖励推广计划 加入成为云推官,推荐新用户注册下单得现金奖励
免费活动
  • 免费试用中心 HOT 多款云产品免费试用,快来开启云上之旅
  • 天翼云用户体验官 NEW 您的洞察,重塑科技边界

智算服务

打造统一的产品能力,实现算网调度、训练推理、技术架构、资源管理一体化智算服务
智算云(DeepSeek专区)
科研助手
  • 算力商城
  • 应用商城
  • 开发机
  • 并行计算
算力互联调度平台
  • 应用市场
  • 算力市场
  • 算力调度推荐
一站式智算服务平台
  • 模型广场
  • 体验中心
  • 服务接入
智算一体机
  • 智算一体机
大模型
  • DeepSeek-R1-昇腾版(671B)
  • DeepSeek-R1-英伟达版(671B)
  • DeepSeek-V3-昇腾版(671B)
  • DeepSeek-R1-Distill-Llama-70B
  • DeepSeek-R1-Distill-Qwen-32B
  • Qwen2-72B-Instruct
  • StableDiffusion-V2.1
  • TeleChat-12B

应用商城

天翼云精选行业优秀合作伙伴及千余款商品,提供一站式云上应用服务
进入甄选商城进入云市场创新解决方案
办公协同
  • WPS云文档
  • 安全邮箱
  • EMM手机管家
  • 智能商业平台
财务管理
  • 工资条
  • 税务风控云
企业应用
  • 翼信息化运维服务
  • 翼视频云归档解决方案
工业能源
  • 智慧工厂_生产流程管理解决方案
  • 智慧工地
建站工具
  • SSL证书
  • 新域名服务
网络工具
  • 翼云加速
灾备迁移
  • 云管家2.0
  • 翼备份
资源管理
  • 全栈混合云敏捷版(软件)
  • 全栈混合云敏捷版(一体机)
行业应用
  • 翼电子教室
  • 翼智慧显示一体化解决方案

合作伙伴

天翼云携手合作伙伴,共创云上生态,合作共赢
天翼云生态合作中心
  • 天翼云生态合作中心
天翼云渠道合作伙伴
  • 天翼云代理渠道合作伙伴
天翼云服务合作伙伴
  • 天翼云集成商交付能力认证
天翼云应用合作伙伴
  • 天翼云云市场合作伙伴
  • 天翼云甄选商城合作伙伴
天翼云技术合作伙伴
  • 天翼云OpenAPI中心
  • 天翼云EasyCoding平台
天翼云培训认证
  • 天翼云学堂
  • 天翼云市场商学院
天翼云合作计划
  • 云汇计划
天翼云东升计划
  • 适配中心
  • 东升计划
  • 适配互认证

开发者

开发者相关功能入口汇聚
技术社区
  • 专栏文章
  • 互动问答
  • 技术视频
资源与工具
  • OpenAPI中心
开放能力
  • EasyCoding敏捷开发平台
培训与认证
  • 天翼云学堂
  • 天翼云认证
魔乐社区
  • 魔乐社区

支持与服务

为您提供全方位支持与服务,全流程技术保障,助您轻松上云,安全无忧
文档与工具
  • 文档中心
  • 新手上云
  • 自助服务
  • OpenAPI中心
定价
  • 价格计算器
  • 定价策略
基础服务
  • 售前咨询
  • 在线支持
  • 在线支持
  • 工单服务
  • 建议与反馈
  • 用户体验官
  • 服务保障
  • 客户公告
  • 会员中心
增值服务
  • 红心服务
  • 首保服务
  • 客户支持计划
  • 专家技术服务
  • 备案管家

了解天翼云

天翼云秉承央企使命,致力于成为数字经济主力军,投身科技强国伟大事业,为用户提供安全、普惠云服务
品牌介绍
  • 关于天翼云
  • 智算云
  • 天翼云4.0
  • 新闻资讯
  • 天翼云APP
基础设施
  • 全球基础设施
  • 信任中心
最佳实践
  • 精选案例
  • 超级探访
  • 云杂志
  • 分析师和白皮书
  • 天翼云·创新直播间
市场活动
  • 2025智能云生态大会
  • 2024智算云生态大会
  • 2023云生态大会
  • 2022云生态大会
  • 天翼云中国行
天翼云
  • 活动
  • 智算服务
  • 产品
  • 解决方案
  • 应用商城
  • 合作伙伴
  • 开发者
  • 支持与服务
  • 了解天翼云
      • 文档
      • 控制中心
      • 备案
      • 管理中心

      JavaEE: 死锁问题详解

      首页 知识中心 软件开发 文章详情页

      JavaEE: 死锁问题详解

      2024-11-04 09:33:40 阅读次数:27

      内存,线程

      •  

      死锁的出现场景

      1. 一个线程一把锁,这个线程针对这把锁,连续加锁了两次

      死锁的场景1:

      void func() {
      	//第一次能够加锁成功
      	synchronized (this) {
      		//第二次加锁的时候,锁对象已经被占用了
      		//第二次加锁就应该阻塞
      		synchronized (this) {
      		
      		}
      	}
      }
      

      这个情况在代码实例中,并没有出现死锁,这是因为synchronized针对这个情况做了特殊处理~

      C++ / Python中的锁就没有这样的功能,就会死锁(借助第三方库可以实现不出现死锁)

      synchronized 是 “可重入锁”, 针对上述一个线程连续加锁两次的情况,synchronized 在加锁的时候,不仅需要判定当前的锁是否是被占用的状态,还要在锁中额外记录一下当前是哪个线程对这个锁加锁了~
      对于可重入锁来说,发现加锁的线程就是当前锁的持有线程,并不会真正进行任何的加锁操作,也不会进行任何的"阻塞操作",而是直接往下执行.
      JavaEE: 死锁问题详解

      那么问题就来了,计算机是怎么知道哪一个是需要真正释放锁的操作呢,换句话说,计算机是怎么知道哪一个是最外层的括号呢 ?

      • 针对上述问题,我们可以引入一个计数器~
        初始情况下,计数器是0
        每次执行到 { 计数器 +1
        每次执行到 } 计数器 -1
        如果某次 -1 后,计数器为0了,那么就说明这次就要真正的释放锁了~
        JavaEE: 死锁问题详解

      这是计算机中非常常见的思想方法,它在JVM中的垃圾回收机制,C++智能指针,Linux等等都用到了.

      2. 两个线程,两把锁

      死锁的场景2:

      1. 首先线程1 现针对 A 加锁,线程2 针对 B 加锁
      2. 之后线程1 不释放锁A 的情况下,再针对 B 加锁.同时线程 2 不释放 B 的情况下针对 A 加锁

      也就是说出现了"循环依赖".

      举个例子:
      程序员来到公司楼下,被保安拦住了.
      保安: 请出示一码通.
      程序员: 我得上楼,修了bug,才能出示一码通.
      保安: 你得出示一码通,才能上楼.┗( ▔, ▔ )┛

      写成代码:

      public class Demo12 {
          private static String lock1 = "";//锁1
          private static String lock2 = "1";//锁2
      
          public static void main(String[] args) throws InterruptedException {
          	//线程1
              Thread t1 = new Thread(()->{
                  synchronized(lock1) {
                      System.out.println("t1 lock1");
      
      				//这里的sleep是为了确保t1和t2都分别拿到lock1和lock2,然后再分别拿对方的锁
      				//如果没有sleep,那么执行顺序就不可控,可能会出现某个线程一口气拿到两把锁,另一个线程还没执行呢,无法构造出死锁
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                      }
                      synchronized(lock2) {
                          System.out.println("t1 lock2");
                          //没有打印出来.说明被线程1被阻塞了
                      }
                  }
              });
      
      		//线程2
              Thread t2 = new Thread(()->{
                  synchronized(lock2) {
                      System.out.println("t2 lock2");
                      try {
                          Thread.sleep(1000);
                      } catch(InterruptedException e) {
                          throw new RuntimeException(e);
                      }
                      synchronized(lock1) {
                          System.out.println("t2 lock1");
                          //没有打印出来.说明被线程2被阻塞了
                      }
                  }
              });
      
              t1.start();
              t2.start();
          }
      }
      

      JavaEE: 死锁问题详解

      3. N个线程 , M个锁

      有一个经典模型: 哲学家就餐问题

      有5个哲学家坐在一块吃面条,任意一个哲学家想要吃到面条都需要拿起左手和右手的筷子~
      这5个哲学家会做两件事:

      1. 思考人生,放下手里的筷子
      2. 吃面条,拿起左右手两边的筷子.

      JavaEE: 死锁问题详解

      通常情况下,这个模型是可以运转的,但是一旦出现极端情况,就会死锁.
      比如,每个哲学家同时拿左手边的筷子,此时每个筷子都被拿起来了,哲学家的右手就拿不起筷子了(因为桌子上没有了),由于哲学家非常固执,当他吃不到面条的时候,也绝对不会放下左手的筷子.
      于是谁都吃不到面条(哲学家: 没错我就是这么固执 o(´^`)o).

      想一想该如何解决上述问题呢?
      很简单,给每个筷子编个号(1,2,3,…,N),然后让所有的哲学家先拿起编号小的筷子,后拿起编号大的筷子.
      JavaEE: 死锁问题详解
      只要遵守上述的拿起筷子的顺序,无论接下来这个模型的运行顺序如何,无论出现多么极端的情况,都不会再死锁了.

      把哲学家看做线程,把筷子看做锁,这就是死锁的第三种情况了~

      4. 内存可见性

      内存可见性问题是指: 当一个线程对共享变量进行了修改后,其他线程可能无法立即看到这个修改。

      这么说可能有点抽象,举个栗子:

      import java.util.Scanner;
      
      public class Demo13 {
          private static int n = 0;
          public static void main(String[] args) throws InterruptedException{
              Thread t1 = new Thread(()->{
                  while (n == 0) {
                      //啥都不写
                  }
                  System.out.println("t1线程结束");
              });
              Thread t2 = new Thread(()->{
                  Scanner scanner = new Scanner(System.in);
                  System.out.print("输入n的值: ");
                  n = scanner.nextInt();
                 System.out.println("t2线程结束");
              });
              t1.start();
              t2.start();
              t1.join();
              t2.join();
              System.out.println("主线程结束");
          }
      }
      

      运行结果:
      JavaEE: 死锁问题详解
      这这这,这不对吧,我们不是已经输入非0的值了吗,n应该不是0了呀,线程t1中的循环的条件不成立了,t1应该结束啊.
      但是实际上,我们输入10后,t1没有任何动静!!
      通过jconsole看到t1线程(Thread-0)仍然是持续工作的~
      JavaEE: 死锁问题详解
      出现上述问题的原因,就是"内存可见性问题"

      为什么会出现内存可见性问题呢?

      Thread t1 = new Thread(()->{
                  while (n == 0) {
                      //啥都不写
                  }
                  System.out.println("t1线程结束");
              });
      

      在t1线程中的循环会执行非常多次, 每次循环都需要执行n == 0 这样的判定,

      1. 从内存读取数据到寄存器中(读取内存,相比之下,这个操作执行的速度非常慢)
      2. 通过类似于cmp指令,比较寄存器和0的值(这个指令的执行速度非常快)

      对于计算机来说,存储数据的设备,有一下几个层次

      1. CPU寄存器: 空间小,速度快,成本高,数据掉电后丢失
      2. 内存: 空间中等,速度中等,数据掉电后丢失
      3. 硬盘: 空间大,速度慢,成本低,数据掉电后不丢失

      每一个层次之间大约相差3~4个数量级

      此时JVM执行这个代码的时候,发现:
      每次循环的过程中,执行"读取内存"这个操作,开销非常大.
      而且每次执行"读取内存"这个操作,结果都是一样的呀.
      并且JVM根本没有意识到,用户可能在未来会修改n
      于是JVM就做了个大胆的操作—直接把"读取内存"这个操作给优化掉了.

      每次循环,不会重新读取内存中的数据,而是直接读取寄存器/cache中的数据(缓存中的结果)

      当JVM做出上述决定之后,此时意味着循环的开销大幅度降低了~
      但是当用户修改n的时候,内存中的n已经改变了
      但是由于t1线程每次循环,不会真的读内存,于是就感知不到n的改变

      这样就引起了bug — “内存可见性问题”

      内存可见性问题,本质上,是编译器/JVM 对代码进行优化的时候,优化出bug了
      如果代码是单线程的,编译器/JVM对代码的优化一般是非常准确的,优化之后,不会影响到逻辑.

      但是代码如果是多线程的,编译器/JVM 的代码优化,就有可能出现误判(编译器 / JVM的bug)
      导致不该优化的地方,也给优化了,于是就造成了内存可见性问题.

      说点题外话:

      编译器为啥要做上述的代码优化,为啥不老老实实地按照程序员写的代码,一板一眼的执行呢?

      主要是因为,有的程序员,写出来的代码,太低效了.
      为了能够降低程序员的门槛,即使你代码写的一般,最终的执行速度也不会落下风.
      因此,主流编译器,都会引入优化机制.
      也就是说,编译器会自动调整你的代码,使其在保持原有逻辑不变的情况下,提高代码的执行效率.

      编译器优化,本身也是一个复杂的话题
      某个代码,何时优化,优化到啥程度,都不好说~
      开放编译器的大佬们,有一系列的策略来实现这里的优化功能.
      咱们站在外行人的角度,是很难判断某个代码是否优化的. 代码稍微改变一点,优化结果就会截然不同~

      解决方法 volatile关键字

      如果我们希望代码正常运行,该咋办呢[・ヘ・?]

      说白了,之所以会出现"内存可见性问题",这不就是因为编译器优化出bug了吗,我们告诉编译器:“誒,你别优化这里~”.不就可以啦!
      锵锵锵锵,"volatile"关键字就可以做到上述操作!

      volatile关键字: 修饰一个变量,提示编译器说,这个变量是"易变"的.
      编译器进行上述优化的前提,是编译器认为针对这个变量的频繁读取,结果都是固定的.

      对变量加上volatile关键字后,编译器就会禁止上述的优化,从而确保每次循环都从内存中重新读取数据~

      对volatile关键字更进一步的理解:
      在引入volatile关键字后,编译器在生成这个代码的时候,就会在这个变量的读取操作附近生成一些特殊的指令,称为"内存屏障".
      后续JVM执行到这些特殊指令,就知道了,不能进行上述优化了~

      总结

      synchronized:

      • 是可重入锁
      • 可重入锁内部记录了当前是哪个线程持有的锁,后续加锁的时候都会进行判定~
      • 它还会通过一个引用计数,来维护当前的加锁次数,从而描述出何时真正释放锁.

      死锁的四个必要条件(缺一不可)[重点]:

      1. 锁是互斥的[锁的基本特性]
      2. 锁是不可抢占的,线程1拿到了锁A,如果线程1不主动释放A,线程2是不能把锁A抢过来的 [锁的基本特性]
      3. 请求和保持.线程1拿到锁之后,不释放A的前提下,去拿锁B [代码结构](我们在写代码时要避免出现锁的嵌套.)
      4. 循环等待 / 环路等待 / 循环依赖. 多个线程获取锁的过程,存在 循环等待~[代码结构]

      假设代码按照请求和保持的方式,获取到N个锁,那么该如何避免出现循环等待呢?
      一个简单有效的办法: 给锁编号,并约定所有的线程在加锁的时候都必须按照一定的顺序来加锁.

      内存可见性问题:

      内存可见性问题是指: 当一个线程对共享变量进行了修改后,其他线程可能无法立即看到这个修改。

      出现内存可见性问题的原因是编译器会对代码进行优化,结果给整出bug了.

      volatile 关键字:修饰某个指定的变量,告诉编译器,这个变量的值是"易变"的,编译器看到这个标志,就不会把读取内存操作,优化成读取寄存器 / cache.也就是说,volatile可以保持指定的变量,对应的内存,总是可见的~

      版权声明:本文内容来自第三方投稿或授权转载,原文地址:https://blog.csdn.net/qrwitu142857/article/details/140914217,作者:月临水,版权归原作者所有。本网站转在其作品的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如因作品内容、版权等问题需要同本网站联系,请发邮件至ctyunbbs@chinatelecom.cn沟通。

      上一篇:C语言:文件操作

      下一篇:JavaEE: 深入探索TCP网络编程的奇妙世界(一)

      相关文章

      2025-05-16 09:15:17

      Linux系统基础-多线程超详细讲解(5)_单例模式与线程池

      Linux系统基础-多线程超详细讲解(5)_单例模式与线程池

      2025-05-16 09:15:17
      单例 , 线程 , 队列
      2025-05-14 10:33:31

      计算机小白的成长历程——数组(1)

      计算机小白的成长历程——数组(1)

      2025-05-14 10:33:31
      strlen , 个数 , 元素 , 内存 , 十六进制 , 地址 , 数组
      2025-05-14 10:33:25

      超级好用的C++实用库之环形内存池

      环形内存池是一种高效的内存管理技术,特别适合于高并发、实时性要求高的系统中,比如:网络服务器、游戏引擎、实时音视频等领域。

      2025-05-14 10:33:25
      buffer , CHP , 内存 , 分配 , 加锁
      2025-05-14 10:07:38

      超级好用的C++实用库之互斥锁

      互斥锁是一种用于多线程编程的同步机制,其主要目的是确保在并发执行环境中,同一时间内只有一个线程能够访问和修改共享资源。

      2025-05-14 10:07:38
      CHP , Lock , 互斥 , 线程 , 释放 , 锁定
      2025-05-14 10:07:38

      30天拿下Rust之所有权

      在编程语言的世界中,Rust凭借其独特的所有权机制脱颖而出,为开发者提供了一种新颖而强大的工具来防止内存错误。这一特性不仅确保了代码的安全性,还极大地提升了程序的性能。

      2025-05-14 10:07:38
      data , Rust , 内存 , 函数 , 变量 , 数据
      2025-05-14 10:03:13

      超级好用的C++实用库之线程基类

      在C++中,线程是操作系统能够进行运算调度的最小单位。一个进程可以包含多个线程,这些线程共享进程的资源,比如:内存空间和系统资源,但它们有自己的指令指针、堆栈和局部变量等。

      2025-05-14 10:03:13
      Linux , void , Windows , 函数 , 操作系统 , 线程
      2025-05-14 10:02:58

      Linux top 命令使用教程

      Linux top 是一个在Linux和其他类Unix 系统上常用的实时系统监控工具。它提供了一个动态的、交互式的实时视图,显示系统的整体性能信息以及正在运行的进程的相关信息。

      2025-05-14 10:02:58
      CPU , 信息 , 内存 , 占用 , 备注 , 进程
      2025-05-14 10:02:48

      互斥锁解决redis缓存击穿

      在高并发系统中,Redis 缓存是一种常见的性能优化方式。然而,缓存击穿问题也伴随着高并发访问而来。

      2025-05-14 10:02:48
      Redis , 互斥 , 数据库 , 线程 , 缓存 , 请求
      2025-05-14 10:02:48

      使用JavaScript打印网页占用内存:详细指南

      在前端开发中,了解网页的内存占用情况对于优化性能和提高用户体验至关重要。

      2025-05-14 10:02:48
      JavaScript , 内存 , 占用 , 泄漏 , 浏览器 , 监听器 , 示例
      2025-05-14 09:51:15

      java循环创建对象内存溢出怎么解决

      在Java中,如果在循环中不当地创建大量对象而不及时释放内存,很容易导致内存溢出(OutOfMemoryError)。

      2025-05-14 09:51:15
      内存 , 占用 , 对象 , 引用 , 循环 , 次数 , 溢出
      查看更多
      推荐标签

      作者介绍

      天翼云小翼
      天翼云用户

      文章

      33561

      阅读量

      5239422

      查看更多

      最新文章

      Linux系统基础-多线程超详细讲解(5)_单例模式与线程池

      2025-05-16 09:15:17

      超级好用的C++实用库之环形内存池

      2025-05-14 10:33:25

      超级好用的C++实用库之互斥锁

      2025-05-14 10:07:38

      超级好用的C++实用库之线程基类

      2025-05-14 10:03:13

      互斥锁解决redis缓存击穿

      2025-05-14 10:02:48

      使用JavaScript打印网页占用内存:详细指南

      2025-05-14 10:02:48

      查看更多

      热门文章

      Java线程同步synchronized wait notifyAll

      2023-04-18 14:15:05

      游戏编程之十一 图像页CPICPAGE介绍

      2022-11-28 01:25:04

      操作系统中的线程种类

      2023-04-24 11:27:18

      Android Priority Job Queue (Job Manager):线程任务的容错重启机制(二)

      2024-09-25 10:13:46

      Android Priority Job Queue (Job Manager):多重不同Job并发执行并在前台获得返回结果(四)

      2023-04-13 09:54:33

      C/C++ 动态解密释放ShellCode

      2023-06-19 06:57:29

      查看更多

      热门标签

      java Java python 编程开发 代码 开发语言 算法 线程 Python html 数组 C++ 元素 javascript c++
      查看更多

      相关产品

      弹性云主机

      随时自助获取、弹性伸缩的云服务器资源

      天翼云电脑(公众版)

      便捷、安全、高效的云电脑服务

      对象存储

      高品质、低成本的云上存储服务

      云硬盘

      为云上计算资源提供持久性块存储

      查看更多

      随机文章

      JAVA设计模式之单例模式详细分析(全)

      Java 和 C++ 的性能对比分析

      java中的String、StringBuffer和StringBuilder的详细分析

      Java中的线程池调优与性能提升技巧

      C++线程的移动语义

      Java并发基础:Executor接口和Executors类的区别

      • 7*24小时售后
      • 无忧退款
      • 免费备案
      • 专家服务
      售前咨询热线
      400-810-9889转1
      关注天翼云
      • 旗舰店
      • 天翼云APP
      • 天翼云微信公众号
      服务与支持
      • 备案中心
      • 售前咨询
      • 智能客服
      • 自助服务
      • 工单管理
      • 客户公告
      • 涉诈举报
      账户管理
      • 管理中心
      • 订单管理
      • 余额管理
      • 发票管理
      • 充值汇款
      • 续费管理
      快速入口
      • 天翼云旗舰店
      • 文档中心
      • 最新活动
      • 免费试用
      • 信任中心
      • 天翼云学堂
      云网生态
      • 甄选商城
      • 渠道合作
      • 云市场合作
      了解天翼云
      • 关于天翼云
      • 天翼云APP
      • 服务案例
      • 新闻资讯
      • 联系我们
      热门产品
      • 云电脑
      • 弹性云主机
      • 云电脑政企版
      • 天翼云手机
      • 云数据库
      • 对象存储
      • 云硬盘
      • Web应用防火墙
      • 服务器安全卫士
      • CDN加速
      热门推荐
      • 云服务备份
      • 边缘安全加速平台
      • 全站加速
      • 安全加速
      • 云服务器
      • 云主机
      • 智能边缘云
      • 应用编排服务
      • 微服务引擎
      • 共享流量包
      更多推荐
      • web应用防火墙
      • 密钥管理
      • 等保咨询
      • 安全专区
      • 应用运维管理
      • 云日志服务
      • 文档数据库服务
      • 云搜索服务
      • 数据湖探索
      • 数据仓库服务
      友情链接
      • 中国电信集团
      • 189邮箱
      • 天翼企业云盘
      • 天翼云盘
      ©2025 天翼云科技有限公司版权所有 增值电信业务经营许可证A2.B1.B2-20090001
      公司地址:北京市东城区青龙胡同甲1号、3号2幢2层205-32室
      • 用户协议
      • 隐私政策
      • 个人信息保护
      • 法律声明
      备案 京公网安备11010802043424号 京ICP备 2021034386号