【深入理解 volatile】内存可见性与同步机制详解
1. 引言
在多线程编程中,共享变量的可见性和同步问题一直是开发者面临的挑战。Java 提供了 volatile
关键字来确保变量的可见性和有序性,但它并不保证原子性。本文将深入探讨 volatile
的工作原理,包括:
- 高速缓存(CPU Cache)和主内存(Main Memory)的同步时机
- 内存屏障(Memory Barrier)的作用
- volatile 的适用场景与限制
- 底层硬件(如 MESI 协议)如何支持 volatile 语义
最后,我们会通过 示例代码 和 内存模型图示 来直观理解 volatile
的行为。
2. volatile 的核心作用
volatile
主要解决两个问题:
- 可见性问题:确保一个线程对变量的修改能立即被其他线程看到。
- 有序性问题:防止 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
写操作:
- 当前 CPU 将缓存行标记为 Modified (M)。
- 其他 CPU 的缓存行被标记为 Invalid (I),强制它们下次读取时重新加载。
volatile
读操作:
- 如果缓存行状态为 Invalid (I),则从主内存加载最新值。
- 否则,直接从缓存读取(但
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++; // 非原子操作,仍可能发生竞态条件
应改用
AtomicInteger
或synchronized
。
7. 总结
特性 | volatile | synchronized | AtomicXXX |
---|---|---|---|
可见性 | |||
有序性 | |||
原子性 | |||
适用场景 | 状态标志、单次写入 | 复杂同步 | 计数器等 |
关键结论:
volatile
保证 写操作后立即同步到主内存,读操作前强制从主内存加载。- 通过 内存屏障 实现,避免指令重排序。
- 不保证原子性,复合操作仍需额外同步。
- 底层依赖 MESI 协议 维护缓存一致性。
8. 扩展思考
volatile
vsfinal
:final
变量在构造函数完成后对所有线程可见,但之后不能修改。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】内存可见性与同步机制详解的更多相关文章
- 转载:futex同步机制详解
在编译2.6内核的时候,你会在编译选项中看到[*] Enable futex support这一项,上网查,有的资料会告诉你"不选这个内核不一定能正确的运行使用glibc的程序", ...
- java synchronized 线程同步机制详解
Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并发线程访问同一个对象object中的这个synchronized(this ...
- 《深入理解mybatis原理》 Mybatis初始化机制详解
对于任何框架而言,在使用前都要进行一系列的初始化,MyBatis也不例外.本章将通过以下几点详细介绍MyBatis的初始化过程. 1.MyBatis的初始化做了什么 2. MyBatis基于XML配置 ...
- 深入理解mybatis原理, Mybatis初始化SqlSessionFactory机制详解(转)
文章转自http://blog.csdn.net/l454822901/article/details/51829785 对于任何框架而言,在使用前都要进行一系列的初始化,MyBatis也不例外.本章 ...
- 《深入理解mybatis原理2》 Mybatis初始化机制详解
<深入理解mybatis原理> Mybatis初始化机制详解 对于任何框架而言,在使用前都要进行一系列的初始化,MyBatis也不例外.本章将通过以下几点详细介绍MyBatis的初始化过程 ...
- Linux 内存机制详解宝典
Linux 内存机制详解宝典 在linux的内存分配机制中,优先使用物理内存,当物理内存还有空闲时(还够用),不会释放其占用内存,就算占用内存的程序已经被关闭了,该程序所占用的内存用来做缓存使用,对于 ...
- 浏览器 HTTP 协议缓存机制详解
最近在准备优化日志请求时遇到了一些令人疑惑的问题,比如为什么响应头里出现了两个 cache control.为什么明明设置了 no cache 却还是发请求,为什么多次访问时有时请求里带了 etag, ...
- JVM的垃圾回收机制详解和调优
JVM的垃圾回收机制详解和调优 gc即垃圾收集机制是指jvm用于释放那些不再使用的对象所占用的内存.java语言并不要求jvm有gc,也没有规定gc如何工作.不过常用的jvm都有gc,而且大多数gc都 ...
- PHP的垃圾回收机制详解
原文:PHP的垃圾回收机制详解 最近由于使用php编写了一个脚本,模拟实现了一个守护进程,因此需要深入理解php中的垃圾回收机制.本文参考了PHP手册. 在理解PHP垃圾回收机制(GC)之前,先了解一 ...
- Java 反射 设计模式 动态代理机制详解 [ 转载 ]
Java 反射 设计模式 动态代理机制详解 [ 转载 ] @author 亦山 原文链接:http://blog.csdn.net/luanlouis/article/details/24589193 ...
随机推荐
- study Python3【2】导入模块
import 与 from...import 在 python 用 import 或者 from...import 来导入相应的模块. 将整个模块(somemodule)导入,格式为: import ...
- 在web.xml下配置springmvc的核心控制器
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" ...
- 技术-Todo
本文描述下一步调研的技术系统 技术 地址 状态 数据库中间件 https://vitess.io/zh/ Todo
- 解决微信二维码接口接口返回:errcode\":47001,\"errmsg\":\"data format error rid: xxx和处理返回的buffer的问题
data format error rid问题: 在php中使用curl调用微信二维码生成接口getwxacodeunlimit时得到错误响应信息: errcode\":47001,\&qu ...
- c#使用内存映射像处理内存一样去快速处理文件
在 .NET Core 中,`System.IO.MemoryMappedFiles.MemoryMappedFile` 类提供了对内存映射文件的支持.通过将文件映射到内存,你可以在应用程序中直接访问 ...
- nim 语言实现迭代器
nim语言默认是支持 for x in items 这样的迭代的,而且一个类如果要支持迭代,可以用 yield 关键字,其实在 nim 主页上第二个例子就已经重点介绍了. # Thanks to Ni ...
- GoView:Start14.6k,上车啦上车啦,Vue3低代码平台GoView,零代码+全栈框架
GoView:Start14.6k,上车啦上车啦,Vue3低代码平台GoView,零代码+全栈框架 项目介绍 GoView 是一个Vue3搭建的低代码数据可视化开发平台,将图表或页面元素封装为基础组件 ...
- ArrayList中的contains方法
ArrayList类的contains方法 如果此 collection 包含指定的元素,则返回 true. 具体实现 public boolean contains(Object o) { retu ...
- 制作netease-cloud-music-gtk的debian包
要创建一个deb包,只需要有一个基于 debian 的操作系统即可.(不管你用的是什么 Linux 发行版,你可以使用虚拟机或者 systemd-nspawn 来创建构建 DEB 包的环境) 下载上游 ...
- 【代码】Android|获取压力传感器、屏幕压感数据(大气压、原生和Processing)
首先需要分清自己需要的是大气压还是触摸压力,如果是大气压那么就是TYPE_PRESSURE,可以参考https://source.android.google.cn/docs/core/interac ...