当前位置: 首页 > news >正文

揭秘volatile关键字:让Java并发编程不再“卡壳”

文章目录

    • 为什么需要volatile?先看看并发编程的“坑”
    • volatile的两大“超能力”
      • 1. 可见性保证
      • 2. 禁止指令重排序
    • volatile的实现原理:底层探秘
      • 内存屏障:volatile的“守护神”
      • 硬件层面的支持:LOCK前缀指令
    • volatile的局限性:它不是什么都能做
    • volatile的应用场景:恰到好处的使用
      • 1. 状态标志
      • 2. 一次性安全发布
      • 3. 独立观察模式
      • 4. volatile bean模式
    • 总结:正确使用volatile的要点
    • 参考文章

掌握volatile关键字,告别可见性与有序性困扰

大家好,我是你们的技术老友“科威舟”——今天我们来聊聊Java并发编程中的“小身材,大能量”的volatile关键字。它在多线程编程中扮演着至关重要的角色,却经常被误解或低估。让我们一起 揭开它的神秘面纱!

为什么需要volatile?先看看并发编程的“坑”

想象一下,你和你的小伙伴共同编辑一份在线文档(共享变量),你修改了内容,但你的小伙伴却看不到最新版本,这会造成什么后果?这就是典型的可见性问题。

在并发编程中,每个线程都有自己的工作内存(相当于CPU缓存),当一个线程修改了共享变量,其他线程不一定能立即看到这个修改。这就像你更新了在线文档,但你的同事仍然看到的是缓存中的旧版本。

除了可见性问题,还有指令重排序的陷阱。编译器和处理器为了优化性能,可能会重新排序指令执行顺序,这在单线程下没问题,但在多线程环境下可能导致意想不到的结果。

volatile的两大“超能力”

volatile关键字虽然看起来简单,但它具备两项重要特性,堪称并发编程的“双刃剑”。

1. 可见性保证

当一个变量被声明为volatile后,对该变量的任何写操作都会立即刷新到主内存中,而对该变量的读操作都会从主内存中读取

这就好比有一个严格的图书管理员:每当有人还书(写操作),他立即将书放回正确位置;每当有人借书(读操作),他确保给出的是最新的版本。

publicclassVolatileExample{privatestaticvolatilebooleanflag=false;publicstaticvoidmain(String[]args){ThreadwriterThread=newThread(()->{try{Thread.sleep(1000);// 模拟一些工作flag=true;// 写入volatile变量System.out.println("标志位已设置为true");}catch(InterruptedExceptione){e.printStackTrace();}});ThreadreaderThread=newThread(()->{while(!flag){// 循环直到检测到flag变为true}System.out.println("检测到标志位变化,线程退出");});readerThread.start();writerThread.start();}}

在这个例子中,如果没有volatile,readerThread可能永远检测不到flag的变化,导致无限循环。而使用volatile后,可以确保可见性。

2. 禁止指令重排序

volatile的第二个魔法是禁止指令重排序。编译器和处理器的重排序优化,在单线程环境下没有问题,但在多线程环境下可能导致诡异的问题。

最经典的例子就是双重检查锁定(DCL)单例模式:

publicclassSingleton{privatestaticvolatileSingletoninstance;privateSingleton(){// 私有构造函数}publicstaticSingletongetInstance(){if(instance==null){// 第一次检查synchronized(Singleton.class){if(instance==null){// 第二次检查instance=newSingleton();// 注意这里!}}}returninstance;}}

为什么instance需要volatile?因为instance = new Singleton()这行代码包含三个步骤:

  1. 分配对象内存空间
  2. 初始化对象
  3. 将引用指向分配的内存地址

如果没有volatile,步骤2和3可能被重排序,导致其他线程获取到未完全初始化的对象!使用volatile可以防止这种重排序。

volatile的实现原理:底层探秘

现在,让我们深入底层,看看volatile是如何实现这些神奇特性的。

内存屏障:volatile的“守护神”

volatile的关键实现机制是内存屏障(Memory Barrier)。内存屏障是一种CPU指令,用于控制特定操作顺序,就像一道屏障,确保屏障前后的指令不会越过它执行。

JVM在volatile读写操作前后插入内存屏障:

  • 在volatile写操作前后

    • 前面插入StoreStore屏障:禁止上面的普通写与volatile写重排序
    • 后面插入StoreLoad屏障:禁止volatile写与下面可能的volatile读/写重排序
  • 在volatile读操作前后

    • 后面插入LoadLoad屏障:禁止下面的普通读与volatile读重排序
    • 后面插入LoadStore屏障:禁止下面的普通写与volatile读重排序

硬件层面的支持:LOCK前缀指令

在x86架构下,volatile的写操作会生成带有LOCK前缀的指令。这个LOCK前缀可不是锁总线那么简单,现代CPU使用缓存一致性协议(如MESI协议)来实现。

当CPU发现要操作的变量被volatile修饰时:

  1. 会将当前处理器缓存行的数据立即写回主内存
  2. 这个写回操作会使其他CPU中缓存该内存地址的数据无效

这就像在一个团队会议上,当某人更新了共享文档后,立即通知所有人:“文档已更新,请重新加载!”

volatile的局限性:它不是什么都能做

虽然volatile很强大,但它并不是万能的。最关键的局限性是:volatile不能保证原子性

什么是原子性?一个操作要么完全执行,要么完全不执行,中间不会被打断。但volatile不能保证复合操作的原子性。

最经典的例子就是i++操作:

publicclassAtomicityExample{privatestaticvolatileintcount=0;publicstaticvoidmain(String[]args)throwsInterruptedException{Threadt1=newThread(()->{for(inti=0;i<10000;i++){count++;// 这不是原子操作!}});Threadt2=newThread(()->{for(inti=0;i<10000;i++){count++;// 这不是原子操作!}});t1.start();t2.start();t1.join();t2.join();System.out.println("最终结果: "+count);// 可能小于20000}}

为什么volatile不能保证原子性?因为count++实际上包含三个步骤:

  1. 读取count的值
  2. 将值加1
  3. 将新值写回count

在多线程环境下,两个线程可能同时读取到相同的值,然后分别加1并写回,导致结果不符合预期。

如果需要保证原子性,应该使用synchronized、Lock或Atomic类

volatile的应用场景:恰到好处的使用

既然了解了volatile的能力和限制,我们在什么情况下应该使用它呢?

1. 状态标志

最经典的用法是作为一个简单的状态标志:

publicclassTaskRunnerimplementsRunnable{privatevolatilebooleanrunning=true;publicvoidrun(){while(running){// 执行任务}}publicvoidstop(){running=false;}}

这种情况下,使用volatile是完美的,因为只有一个线程修改running标志,多个线程读取它。

2. 一次性安全发布

volatile可以用于安全发布不可变对象:

publicclassResourceFactory{privatevolatileResourceresource;publicResourcegetResource(){if(resource==null){synchronized(this){if(resource==null){resource=newResource();// 安全发布}}}returnresource;}}

3. 独立观察模式

定期"发布"观察结果供程序其他部分使用:

publicclassTemperatureSensor{privatevolatiledoublecurrentTemperature;privatevoidsenseTemperature(){while(true){// 读取温度传感器currentTemperature=readSensor();Thread.sleep(1000);}}publicdoublegetTemperature(){returncurrentTemperature;// 总是读取最新值}}

4. volatile bean模式

在特定情况下,可以将bean的所有成员变量都声明为volatile,但这适用于特定场景。

总结:正确使用volatile的要点

  1. volatile保证可见性:一个线程修改volatile变量,其他线程立即可见
  2. volatile保证有序性:通过内存屏障禁止指令重排序
  3. volatile不保证原子性:复合操作(如i++)仍需其他同步机制
  4. volatile比synchronized更轻量:不会引起线程上下文切换

volatile关键字是Java并发编程中的重要工具,虽然它不能解决所有并发问题,但在适当的场景下,它是一个简单高效的解决方案。理解其底层原理和适用场景,有助于我们编写更安全、高效的多线程程序。

希望本文能帮助你更好地理解和应用volatile关键字!如果你有更好的例子或经验,欢迎在评论区分享。

参考文章

  1. https://bbs.huaweicloud.com/blogs/386846
  2. https://www.cnblogs.com/hanease/p/15864913.html
  3. https://blog.csdn.net/bbj12345678/article/details/120584166
  4. https://juejin.cn/post/7018357942403465246
  5. https://juejin.cn/post/7132479957938225159

更多技术干货欢迎关注微信公众号科威舟的AI笔记~

【转载须知】:转载请注明原文出处及作者信息

http://www.cnnetsun.cn/news/115446.html

相关文章:

  • day38打卡
  • JavaEE进阶——SpringBoot日志从入门到精通
  • 结构体简单题
  • 时间序列回归预测:LSTM、CNN - LSTM、PSO - CNN - LSTM、GAPSO - CNN - LSTM大比拼
  • 飞轮储能系统的建模与 MATLAB 仿真:永磁同步电机作为飞轮驱动电机
  • 车间进度总卡壳?生产小工单的3个必备功能,90%企业都用错了
  • 如何用 ShedLock 让 Spring Boot 的定时任务在多实例环境下只执行一次
  • 基于MPC的永磁同步电机非线性终端滑模控制仿真研究
  • ISSA - CNN - BiLSTM多输入单输出回归的Python实现与改进
  • Q学习(Q-learning)路径规划算法实战
  • ANSYS/LS - dyna防爆涂层砂浆砖框架结构爆破荷载损伤响应案例探索
  • 基于TOA/FOA的无源定位方法MATLAB仿真探索
  • 基于一致性算法改进的自适应虚拟阻抗控制:解决双机并联功率分布不均
  • springboot框架对接物联网,配置TCP协议依赖,与设备通信,让TCP变的如此简单
  • 微软和布朗大学最新发现:让AI助手拥有18000多种技能的革命性突破
  • MATLAB仿真:二维TOA传感器网络定位与时钟偏差拟合,最小二乘求解
  • 【参数辨识】基于卡尔曼滤波(KF)估计离散线性系统对垂直起降(VTOL)飞行器的鲁棒辨识附matlab代码
  • 桥梁与隧道安全守护者 抗冰冻型风速监测方案
  • 05-FreeRTOS的内存管理
  • 基于改进蛇优化算法(GOSO/ISO)优化随机森林数据回归预测模型(含初始化种群混沌映射、减法...
  • 基于大数据的人脸识别系统设计与实现开题报告
  • 车载 Android 系统稳定性问题全解析:从性能到黑屏的排查指南
  • 气象在线监测系统助力智慧环境管理,金叶仪器专业气象监测解决方案
  • 【TVM 教程】交叉编译与 RPC
  • 腾讯云国际站代理商的QAPM服务能提供哪些专属服务?
  • 网安副业怎么选?漏洞挖掘、技术博客、竞赛奖金实战,哪个更适配你?
  • 量子计算验证方法:软件测试从业者的转型指南
  • 突破 Oracle/MySQL 瓶颈:金仓数据库以三重革新,筑牢业务转型 “数据底座”
  • 【学习神器】NotebookLM“播客”功能实战指南:四六级、考研党高效复习秘籍
  • 如何解决 pip install 网络报错 ERROR: No matching distribution found for requests