1. 引言

在多线程编程中,共享变量的可见性和同步问题一直是开发者面临的挑战。Java 提供了 volatile 关键字来确保变量的可见性有序性,但它并不保证原子性。本文将深入探讨 volatile 的工作原理,包括:

  • 高速缓存(CPU Cache)和主内存(Main Memory)的同步时机
  • 内存屏障(Memory Barrier)的作用
  • volatile 的适用场景与限制
  • 底层硬件(如 MESI 协议)如何支持 volatile 语义

最后,我们会通过 示例代码内存模型图示 来直观理解 volatile 的行为。


2. volatile 的核心作用

volatile 主要解决两个问题:

  1. 可见性问题:确保一个线程对变量的修改能立即被其他线程看到。
  2. 有序性问题:防止 JVM 和 CPU 对指令进行不合理的重排序。

但它 不保证原子性(如 i++ 这样的复合操作仍然需要额外的同步机制)。


3. volatile 的同步机制

3.1 何时同步?

Java 内存模型(JMM)规定,volatile 变量的读写遵循严格的规则:

  • 写操作(Write)

    • 当线程写入 volatile 变量时,JVM 会 立即 将该值刷新到主内存(而不是仅停留在 CPU 缓存)。
    • 为了保证立即刷新,JVM 会在写操作后插入 StoreLoad 内存屏障(或等效指令),强制 CPU 将数据写回主内存,并确保后续读操作能看到最新值。
  • 读操作(Read)

    • 当线程读取 volatile 变量时,JVM 会 强制 从主内存加载最新值(而不是使用本地缓存的旧值)。
    • 为了保证读取最新值,JVM 会在读操作前插入 LoadLoad + LoadStore 内存屏障(或等效指令),使当前 CPU 缓存失效并重新加载数据。

3.2 同步流程图

+-------------------+       +-------------------+       +-------------------+
| Thread 1 | | Main Memory | | Thread 2 |
| (CPU Core 1) | | | | (CPU Core 2) |
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| volatile x = 1; | ----> | x = 1 (最新值) | <---- | int y = x; |
| | | | | (读取最新值) |
+-------------------+ +-------------------+ +-------------------+
  • Thread 1 写入 volatile x = 1

    • 值立即写入主内存,而不是仅停留在 Core 1 的缓存。
  • Thread 2 读取 volatile x
    • 强制从主内存加载最新值,而不是使用 Core 2 缓存中的旧值。

4. 内存屏障(Memory Barrier)的作用

内存屏障是 CPU 或 JVM 插入的特殊指令,用于控制指令执行顺序和缓存一致性。volatile 依赖内存屏障实现其语义:

屏障类型 作用
StoreStore 确保 volatile 写之前的普通写操作先完成
StoreLoad 确保 volatile 写完成后,后续读操作能看到最新值
LoadLoad 确保 volatile 读之前的普通读操作先完成
LoadStore 确保 volatile 读完成后,后续写操作不会重排序到读之前

volatile 写操作后的 StoreLoad 屏障是最严格的,因为它强制刷新所有缓存数据到主内存,确保后续读操作能获取最新值。


5. 底层硬件支持(MESI 协议)

现代 CPU 使用 缓存一致性协议(如 MESI)来维护多核缓存的一致性。volatile 的内存屏障会触发 CPU 执行必要的缓存同步操作:

  • MESI 状态

    • Modified (M):当前 CPU 缓存的数据已被修改,与主内存不一致。
    • Exclusive (E):当前 CPU 独占缓存行,数据与主内存一致。
    • Shared (S):多个 CPU 共享缓存行,数据与主内存一致。
    • Invalid (I):缓存行无效,必须从主内存重新加载。

volatile 写操作

  1. 当前 CPU 将缓存行标记为 Modified (M)
  2. 其他 CPU 的缓存行被标记为 Invalid (I),强制它们下次读取时重新加载。

volatile 读操作

  1. 如果缓存行状态为 Invalid (I),则从主内存加载最新值。
  2. 否则,直接从缓存读取(但 volatile 强制读屏障,通常会使缓存失效)。

6. volatile 的适用场景与限制

6.1 适用场景

  • 状态标志(如 boolean running
    volatile boolean running = true;
    
    void stop() { running = false; }
    void doWork() { while (running) { ... } }
  • 单次写入、多次读取(如配置变量)
    volatile Config config = loadConfig();

6.2 不适用场景

  • 复合操作(如 i++
    volatile int count = 0;
    count++; // 非原子操作,仍可能发生竞态条件

    应改用 AtomicIntegersynchronized


7. 总结

特性 volatile synchronized AtomicXXX
可见性
有序性
原子性
适用场景 状态标志、单次写入 复杂同步 计数器等

关键结论:

  1. volatile 保证 写操作后立即同步到主内存读操作前强制从主内存加载
  2. 通过 内存屏障 实现,避免指令重排序。
  3. 不保证原子性,复合操作仍需额外同步。
  4. 底层依赖 MESI 协议 维护缓存一致性。

8. 扩展思考

  • volatile vs finalfinal 变量在构造函数完成后对所有线程可见,但之后不能修改。
  • volatile 在单例模式(DCL)中的应用
    class Singleton {
    private static volatile Singleton instance; public static Singleton getInstance() {
    if (instance == null) {
    synchronized (Singleton.class) {
    if (instance == null) {
    instance = new Singleton();
    }
    }
    }
    return instance;
    }
    }

    这里的 volatile 防止指令重排序,避免返回未初始化的对象。


希望这篇博客能帮助你彻底理解 volatile 的机制!如果有疑问或建议,欢迎在评论区讨论。

【深入理解 volatile】内存可见性与同步机制详解的更多相关文章

  1. 转载:futex同步机制详解

    在编译2.6内核的时候,你会在编译选项中看到[*] Enable futex support这一项,上网查,有的资料会告诉你"不选这个内核不一定能正确的运行使用glibc的程序", ...

  2. java synchronized 线程同步机制详解

    Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并发线程访问同一个对象object中的这个synchronized(this ...

  3. 《深入理解mybatis原理》 Mybatis初始化机制详解

    对于任何框架而言,在使用前都要进行一系列的初始化,MyBatis也不例外.本章将通过以下几点详细介绍MyBatis的初始化过程. 1.MyBatis的初始化做了什么 2. MyBatis基于XML配置 ...

  4. 深入理解mybatis原理, Mybatis初始化SqlSessionFactory机制详解(转)

    文章转自http://blog.csdn.net/l454822901/article/details/51829785 对于任何框架而言,在使用前都要进行一系列的初始化,MyBatis也不例外.本章 ...

  5. 《深入理解mybatis原理2》 Mybatis初始化机制详解

    <深入理解mybatis原理> Mybatis初始化机制详解 对于任何框架而言,在使用前都要进行一系列的初始化,MyBatis也不例外.本章将通过以下几点详细介绍MyBatis的初始化过程 ...

  6. Linux 内存机制详解宝典

    Linux 内存机制详解宝典 在linux的内存分配机制中,优先使用物理内存,当物理内存还有空闲时(还够用),不会释放其占用内存,就算占用内存的程序已经被关闭了,该程序所占用的内存用来做缓存使用,对于 ...

  7. 浏览器 HTTP 协议缓存机制详解

    最近在准备优化日志请求时遇到了一些令人疑惑的问题,比如为什么响应头里出现了两个 cache control.为什么明明设置了 no cache 却还是发请求,为什么多次访问时有时请求里带了 etag, ...

  8. JVM的垃圾回收机制详解和调优

    JVM的垃圾回收机制详解和调优 gc即垃圾收集机制是指jvm用于释放那些不再使用的对象所占用的内存.java语言并不要求jvm有gc,也没有规定gc如何工作.不过常用的jvm都有gc,而且大多数gc都 ...

  9. PHP的垃圾回收机制详解

    原文:PHP的垃圾回收机制详解 最近由于使用php编写了一个脚本,模拟实现了一个守护进程,因此需要深入理解php中的垃圾回收机制.本文参考了PHP手册. 在理解PHP垃圾回收机制(GC)之前,先了解一 ...

  10. Java 反射 设计模式 动态代理机制详解 [ 转载 ]

    Java 反射 设计模式 动态代理机制详解 [ 转载 ] @author 亦山 原文链接:http://blog.csdn.net/luanlouis/article/details/24589193 ...

随机推荐

  1. study Python3【2】导入模块

    import 与 from...import 在 python 用 import 或者 from...import 来导入相应的模块. 将整个模块(somemodule)导入,格式为: import ...

  2. 在web.xml下配置springmvc的核心控制器

    <!DOCTYPE web-app PUBLIC        "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" ...

  3. 技术-Todo

    本文描述下一步调研的技术系统 技术 地址 状态 数据库中间件 https://vitess.io/zh/ Todo

  4. 解决微信二维码接口接口返回:errcode\":47001,\"errmsg\":\"data format error rid: xxx和处理返回的buffer的问题

    data format error rid问题: 在php中使用curl调用微信二维码生成接口getwxacodeunlimit时得到错误响应信息: errcode\":47001,\&qu ...

  5. c#使用内存映射像处理内存一样去快速处理文件

    在 .NET Core 中,`System.IO.MemoryMappedFiles.MemoryMappedFile` 类提供了对内存映射文件的支持.通过将文件映射到内存,你可以在应用程序中直接访问 ...

  6. nim 语言实现迭代器

    nim语言默认是支持 for x in items 这样的迭代的,而且一个类如果要支持迭代,可以用 yield 关键字,其实在 nim 主页上第二个例子就已经重点介绍了. # Thanks to Ni ...

  7. GoView:Start14.6k,上车啦上车啦,Vue3低代码平台GoView,零代码+全栈框架

    GoView:Start14.6k,上车啦上车啦,Vue3低代码平台GoView,零代码+全栈框架 项目介绍 GoView 是一个Vue3搭建的低代码数据可视化开发平台,将图表或页面元素封装为基础组件 ...

  8. ArrayList中的contains方法

    ArrayList类的contains方法 如果此 collection 包含指定的元素,则返回 true. 具体实现 public boolean contains(Object o) { retu ...

  9. 制作netease-cloud-music-gtk的debian包

    要创建一个deb包,只需要有一个基于 debian 的操作系统即可.(不管你用的是什么 Linux 发行版,你可以使用虚拟机或者 systemd-nspawn 来创建构建 DEB 包的环境) 下载上游 ...

  10. 【代码】Android|获取压力传感器、屏幕压感数据(大气压、原生和Processing)

    首先需要分清自己需要的是大气压还是触摸压力,如果是大气压那么就是TYPE_PRESSURE,可以参考https://source.android.google.cn/docs/core/interac ...