【深入理解 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. 扩展思考
volatilevsfinal: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 ...
随机推荐
- 【SpringMVC】映射请求参数 & 请求头
映射请求参数 & 请求参数 请求处理方法签名 Spring MVC 通过分析处理方法的签名,将 HTTP 请求信息绑定到处理方法的相应人参中. Spring MVC 对控制器处理方法签名的限制 ...
- yolov5常用命令记录
一.准备深度学习环境 首先,确保你的计算机上已经安装了Python.PyTorch以及CUDA等必要的深度学习框架和库.YOLOv5对Python版本和PyTorch版本有一定的要求,通常建议使用Py ...
- javaWeb基础之Tomcat
一.Tomcat:web服务器软件 1. 下载:http://tomcat.apache.org/ 2. 安装:解压压缩包即可. * 注意:安装目录建议不要有中文和空格 3. 卸载:删除目录就行了 4 ...
- jmeter之请求体类型
一.当post方法的提交数据类型(content-type)为multipart/form-data,请求体为文件文件上传. fiddler抓包请求体的name对应jmerter文件上传的参数名称,f ...
- ubuntu nginx + php7.2 + mysql5.7环境搭建
一.换源 备份原来的源 sudo cp /etc/apt/sources.list /etc/apt/sources_init.list 更换源 sudo gedit /etc/apt/sources ...
- GBDT算法原理及Python实现
一.概述 GBDT(Gradient Boosting Decision Tree,梯度提升决策树)是集成学习中提升(Boosting)方法的典型代表.它以决策树(通常是 CART 树,即分类回归 ...
- Spring--IOC注解用法初探
创建一个UserDao接口,和一个UserDaoImp的实现类 UserDao接口 package com.zjw.spring.demo1; public interface UserDao { p ...
- 学习unigui【29】UniGUI的RBCA
web程序真是鸡鸣狗盗,零零碎碎. 学习类似SaaS的登录界面,补一大通web的基础知识. http://127.0.0.1:8077和http://127.0.0.1:8077/admin 这是登录 ...
- 操作系统综合题之“用记录型信号量机制的wait和signal操作来解决了由北向南和由南向北过河人的同步问题(独木桥问题-代码补充)”
1.问题:一条哦东西走向河流上,有一根南北走向的独木桥,要想过河只能通过这根独木桥.只要人们朝着相同的方向过独木桥,同一时刻允许有多个人可以通过.如果在相反的方向上同时有两个人过独木桥则会发生死锁.如 ...
- Bolt DIY架构揭秘:从模型初始化到响应生成的技术之旅
Bolt DIY 是一个强大的开源AI辅助开发工具,允许用户在浏览器中进行全栈Web开发.它的核心特点是支持多种大型语言模型(LLM),包括OpenAI.Anthropic.Ollama.Google ...