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

jmm--volatile

volatile 是Java中一个用于解决多线程内存可见性和禁止指令重排序问题的关键字,下面从多个方面深入且易懂地解析它与JVM的关系。

1. 内存可见性

  • 问题背景:在多线程环境下,每个线程都有自己的工作内存,线程对变量的操作(读取、修改)都在工作内存中进行,而不是直接操作主内存中的变量。这就可能导致一个线程对变量的修改,其他线程不能及时看到。例如:
public class VisibilityProblem {private static boolean flag = false;public static void main(String[] args) {new Thread(() -> {while (!flag) {// 线程1在等待flag变为true}System.out.println("线程1结束等待");}).start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {flag = true;System.out.println("线程2修改了flag");}).start();}
}

在上述代码中,线程2修改了 flag 变量,但线程1可能一直无法感知到这个变化,导致线程1无限循环。

  • volatile 的作用:当一个变量被声明为 volatile 时,线程对该变量的修改会立即同步到主内存,并且其他线程在读取该变量时,会强制从主内存中获取最新值,而不是使用自己工作内存中的缓存值。修改上述代码如下:
public class VolatileVisibility {private static volatile boolean flag = false;public static void main(String[] args) {new Thread(() -> {while (!flag) {// 线程1在等待flag变为true}System.out.println("线程1结束等待");}).start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {flag = true;System.out.println("线程2修改了flag");}).start();}
}

此时,线程2修改 flag 后,线程1能及时获取到新值,从而结束循环。

2. 禁止指令重排序

  • 指令重排序概念:为了提高程序执行效率,JVM和处理器会对指令进行优化,其中一种优化方式就是指令重排序。在单线程环境下,指令重排序不会影响最终执行结果,但在多线程环境下可能会导致问题。例如:
public class ReorderingProblem {private static int a = 0;private static int b = 0;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {a = 1; // 语句1b = 2; // 语句2});Thread thread2 = new Thread(() -> {if (b == 2) { // 语句3System.out.println(a); // 语句4}});thread1.start();thread2.start();thread1.join();thread2.join();}
}

在理想情况下,线程1先执行完 a = 1b = 2,线程2执行时 b == 2 为真,会输出 1。但由于指令重排序,线程1可能先执行 b = 2,然后执行 a = 1,此时线程2执行时 b == 2 为真,但 a 可能还未被赋值为 1,输出结果可能为 0

  • volatile 对指令重排序的限制volatile 关键字具有禁止指令重排序的语义。当一个变量被声明为 volatile 时,在 volatile 变量的写操作之前的所有操作,都先行发生于该 volatile 变量的写操作;而 volatile 变量的读操作,先行发生于在其之后的所有操作。这确保了 volatile 变量相关的操作顺序与代码顺序一致。例如:
public class VolatileReordering {private static volatile int a = 0;private static int b = 0;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {a = 1; // 语句1b = 2; // 语句2});Thread thread2 = new Thread(() -> {if (b == 2) { // 语句3System.out.println(a); // 语句4}});thread1.start();thread2.start();thread1.join();thread2.join();}
}

这里 a 被声明为 volatile,保证了语句1在语句2之前执行,并且语句1的结果对线程2可见,从而避免了指令重排序带来的问题。

3. volatile 与JVM内存模型

JVM内存模型定义了主内存和工作内存之间的交互关系,volatile 关键字正是基于这个模型来实现其功能的。

  • volatile 写操作:当一个线程对 volatile 变量进行写操作时,JVM会将该变量在工作内存中的最新值刷新到主内存中,同时会确保在这个写操作之前的所有普通写操作都已经刷新到主内存。
  • volatile 读操作:当一个线程对 volatile 变量进行读操作时,JVM会强制从主内存中读取该变量的值,而不是使用工作内存中的缓存值,并且会确保在这个读操作之后的所有普通读操作都能看到 volatile 变量的最新值。

4. volatile 的使用场景

  • 状态标记:常用于标记某些状态,例如线程的运行状态。如一个线程负责数据加载,另一个线程负责数据处理,数据加载线程完成加载后通过修改 volatile 标记通知处理线程。
public class DataLoader {private static volatile boolean dataLoaded = false;public static void main(String[] args) {new Thread(() -> {// 模拟数据加载try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}dataLoaded = true;}).start();new Thread(() -> {while (!dataLoaded) {// 等待数据加载完成}System.out.println("开始处理数据");}).start();}
}
  • 单例模式中的双重检查锁定(DCL):在实现单例模式时,使用 volatile 可以防止指令重排序导致的对象初始化问题。
public class Singleton {private static volatile Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}

5. volatile 的局限性

  • 不保证原子性volatile 不能保证复合操作的原子性。例如 count++ 操作,它实际上包含读取、增加和写入三个步骤,即使 count 被声明为 volatile,在多线程环境下仍然可能出现数据竞争问题。对于这种情况,需要使用 AtomicInteger 等原子类或者 synchronized 关键字来保证原子性。
public class VolatileAtomicityProblem {private static volatile int count = 0;public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[100];for (int i = 0; i < 100; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < 1000; j++) {count++;}});threads[i].start();}for (Thread thread : threads) {thread.join();}System.out.println("最终count的值: " + count);}
}

上述代码中,由于 count++ 不是原子操作,最终输出的 count 值往往小于预期的 100000

综上所述,volatile 关键字在多线程编程中是一个强大的工具,能够有效解决内存可见性和指令重排序问题,但需要清楚其适用场景和局限性,以确保多线程程序的正确性。

http://www.dtcms.com/a/267732.html

相关文章:

  • 前端面试专栏-算法篇:18. 查找算法(二分查找、哈希查找)
  • vue3 el-input el-select 非空校验
  • 大数据学习2:HIve
  • Linux进程管理:从基础到实战
  • Qt Ribbon效果界面
  • QT6 源(154)模型视图架构里的列表视图 QListView:先学习属性部分,
  • 认识Redis
  • Chat Model API
  • 60天python训练营打卡day52
  • 运算方法和运算器补充
  • 如何录制带备注的演示文稿(LaTex Beamer + Pympress)
  • Codeforces Round 919 (Div. 2) D. Array Repetition(分块,1900)
  • 【深圳大学机器学习】实验一:PCA算法
  • 【ACL系列论文写作指北15-如何进行reveiw】-公平、公正、公开
  • 大数据学习1:Hadoop单机版环境搭建
  • Redis 哨兵模式部署--docker版本
  • C++面试-auto,auto,auto 的区别
  • 【ESP32】2.多任务处理
  • 相机位姿估计
  • 使用接口测试工具类Postman和浏览器的差异
  • C++ 语言特性31 - 协程介绍(2)
  • 用 Turbo Vision 2 为 Qt 6 控制台应用创建 TUI 字符 MainFrame
  • Redis性能优化
  • 五、Python新特性指定类型用法
  • AI大模型(六)Langchain核心模块与实战(一)
  • 使用LVM和扩展文件系统增加root分区存储容量
  • 信号和槽(4)
  • 3dmax物理材质转换标准材质,物理材质转VR材质,VR材质转标准材质3dmax物理材质转标准材质插件
  • 98.验证二叉搜索树
  • python实现简单的地图绘制与标记20250705