第一次学 volatile 关键字,我看了三遍才搞懂它到底在干嘛
学 Java 并发编程的时候,第一个让我卡住的关键字就是 volatile。
网上搜了一圈,全是"可见性"“指令重排序”“内存屏障”——每个字都认识,放在一起我就不知道它们在说什么了。
说实话我看了好几遍,又去翻了点资料,总算理清楚了一点。这篇就当是我自己整理的学习笔记。可能有些地方说得不够严谨,但至少是我自己的理解。如果有哪里写错了,欢迎指出来,我也还在学。
先说 volatile 到底是干嘛的
volatile 是 Java 给变量加的一个关键字。
你不加它,变量在多线程下可能出问题。你加了它,就相当于告诉 JVM:“这个变量你给我小心点处理,别自作聪明。”
它主要做两件事。我分开来说。
第一件事:让一个线程改了,其他线程能看见
这个叫"可见性"。
我打个比方。假设你和几个同事一起做一个项目,项目文档放在共享文件夹里。但是每个人为了方便,都把文档在本地电脑打开了一份。现在你修改了文档,保存了。你的同事看自己本地那版——还是旧的内容。
这就是多线程里的问题。
Java 里每个线程都有自己的"工作内存",相当于本地缓存。线程读变量的时候,先读到自己这边来;改了之后,也不一定马上写回主内存。如果不用 volatile,别的线程可能根本看不到你改了。
加上 volatile 之后,Java 就会保证:你写的时候立即刷回主内存,别人读的时候直接从主内存读。相当于你在共享文档上点了"保存并同步",所有人都能看到最新版。
第二件事:不让编译器和处理器乱调顺序
这个叫"禁止指令重排序",比可见性难理解一点。
先说说指令重排序是怎么回事。
指令重排序是啥
编译器和处理器为了让程序跑得快一点,会在不改变执行结果的前提下,调整指令的执行顺序。
打个比方。你做番茄炒蛋,正常情况下应该是:洗番茄 → 切番茄 → 打蛋 → 炒菜。但如果你先打蛋再洗番茄,结果是一样的,没有人会在意你先做哪一步。编译器和处理器就是这个逻辑。
但这里有一个重要的前提:重排序不能影响单线程的运行结果。
所以 CPU 在做重排序的时候,它会盯着一个原则——数据依赖性。什么意思?就是如果两个操作之间有依赖关系,那就不能重排序。
我举个例子:
操作 A:x = 1 操作 B:y = 2 操作 C:z = x + y操作 C 要用到 x 和 y 的值,所以它必须在 A 和 B 之后执行,这个顺序不会被打破。但是操作 A 和操作 B 之间没有依赖关系——谁先谁后无所谓,反正结果都是 x=1、y=2。所以编译器或者处理器完全可以把顺序调成 B 先执行、A 后执行。
这在单线程下没问题。但是在多线程环境下就不一定了——如果另一个线程在你改 x 和 y 的中间插了一脚,看到了一个"中间状态",可能就出问题了。
volatile 怎么管这件事
volatile 是靠"内存屏障"来管这件事的。
内存屏障这个东西,你可以把它理解成一道栅栏。编译器和处理器再怎么重排序,也不能把指令从栅栏的一边移到另一边去。
Java 针对 volatile 变量,在几个关键位置插了这些栅栏:
- 写之前插一道:保证前面的普通写操作不会被排到 volatile 写之后
- 写之后插一道:保证 volatile 写不会被排到后面的 volatile 读/写之后。这道屏障开销最大
- 读之后插两道:保证 volatile 读不会被排到后面的普通读和普通写之后
说实话,记这些屏障的名字(StoreStore、StoreLoad、LoadLoad、LoadStore)对我来说意义不大。我更关心的是结果:加了 volatile,代码在多线程下就不会因为指令重排序而出一些莫名其妙的问题。
学完还是有点模糊的地方
写这篇的时候我又确认了一下自己的理解,发现有几个地方还是模棱两可的。
比如 volatile 和 synchronized 的区别。我知道 volatile 不能保证原子性——也就是说,如果你对一个 volatile 变量做 i++ 操作(读-改-写三步),它还是可能出问题。但具体到代码里什么时候用 volatile 什么时候用 synchronized,我还需要再多写几个例子才能有感觉。
还有一个让我纠结的点:volatile 的可见性。我看资料说它保证可见性,那是不是加了 volatile 就能保证线程安全了?不是的——刚才说了,它不保证原子性。这两个概念放在一起容易搞混,我也花了好一会儿才理清楚。
不过我觉得这就是学习的过程吧。先有个大概的框架,知道 volatile 能做什么、不能做什么,后面再慢慢往里面填细节。
简单总结一下我自己的理解
volatile 做两件事,不做第三件事:
- 保证可见性:你改了,别人知道
- 禁止重排序:不会乱调执行顺序
- 不保证原子性:i++ 该出问题还是出问题
就这样。没什么花里胡哨的,记住它干什么不干什么,写代码的时候心里就有数了。
这篇是边学边写的。如果有理解不对的地方,欢迎指出来,我改。
