大家好,我是你们的技术小伙伴小米!今天我们来聊一个 Java 社招面试中经常被问到的问题:volatile 能使得一个非原子操作变成原子操作吗?这道题看似简单,实际上却暗藏玄机。如果你觉得 volatile 仅仅是“保证可见性”,那这次你可能要涨涨姿势了!
故事开始:面试现场的灵魂拷问阿康最近在准备 Java 高级开发的社招面试,今天他来到了一家互联网大厂,和面试官聊得还不错。然而,到了关键时刻,面试官微微一笑,丢出了这样一个问题:
“volatile 关键字能让一个非原子操作变成原子操作吗?”
阿康心里想:“volatile 不是保证可见性吗?原子性不是 synchronized 和 Lock 解决的吗?” 但为了谨慎起见,他决定仔细思考一下再回答。
如果你是阿康,你会怎么回答?
volatile 的主要作用我们先复习一下volatile关键字的两个主要作用:
保证变量的可见性(visibility):
当一个变量被 volatile 修饰时,所有线程对这个变量的修改都会立即刷新到主内存中,其他线程读取时能够立刻看到最新值,而不会使用线程本地缓存(CPU 缓存或寄存器)。
禁止指令重排序(ordering):
volatile 变量的读/写操作不能和前后的指令发生重排序,确保代码执行的顺序与程序员的意图一致。
然而,volatile 不能保证原子性。比如下面的代码:
虽然 count 变量被 volatile 修饰,但 count++ 不是原子操作,因为它包含了三个步骤:
读取 count 的值到寄存器
计算 count + 1
将结果写回 count
在多线程环境下,可能会发生线程 A 读取了 count 的值为 5,线程 B 也读取了 count 为 5,然后各自加 1,再分别写回 6,导致最终 count 只加了 1,而不是 2。这就是经典的“竞态条件”(Race Condition)!
所以,阿康回答道:“volatile 不能使非原子操作变成原子操作。”
面试官点点头,接着追问:“但如果是 long 和 double 呢?”
阿康愣了一下,心想:“难道有例外?”
long 和 double 变量的特殊性在 Java 中,大部分变量的读写操作都是原子性的。例如,int、short、char 这些 32 位(4 字节)数据类型,JVM 在读写它们时是原子的,不会被拆分成多步操作。
但对于 long 和 double 这两种 64 位(8 字节)数据类型,JVM 的处理方式略有不同:
如果 long 和 double 没有被 volatile 修饰,JVM 可能会把它们的读写操作拆分成两个 32 位的操作(低 32 位 + 高 32 位),导致多线程环境下读取到错误的数据。
如果用 volatile 修饰 long 或 double,JVM 需要保证它们的读写操作是原子的。
举个例子:
假设线程 A 读取 data,线程 B 可能正在更新 data,如果 JVM 选择分两步操作 long 变量,可能出现这样的情况:
线程 A 读取 data 的低 32 位(0xABCDEF01)
线程 B 更新 data,写入新值(0x1122334455667788)
线程 A 读取 data 的高 32 位(0x11223344)
最终,线程 A 可能会得到一个混合值:0x11223344ABCDEF01,这是完全错误的!
解决办法?
这样,JVM 就会保证对 data 的读/写操作是原子的,不会被拆成两个 32 位的操作。因此,volatile 在这里能保证原子性!
JVM 规范对 long 和 double 读写的选择Oracle 在 Java 规范中明确指出:
JVM 在实现时,可以选择是否把 long 和 double 的读写作为原子操作。推荐的做法是如果没有 volatile,JVM 可能会拆分操作;如果有 volatile,JVM 必须保证原子性。
换句话说,JVM 设计者可以自由决定是否让 long 和 double 变量的读写是原子的,但大多数现代 JVM 实现都会保证它们是原子的,即便没有 volatile,也不会拆成两步操作(比如 OpenJDK)。
但无论如何,用 volatile 修饰 long 和 double,能够确保它们的读写是原子的,这点是写入 Java 规范的。
总结:volatile 不能保证原子性,但有例外面试结束后,阿康回去复盘了一下,得出了以下结论:
volatile 主要作用是可见性和禁止指令重排序,但不能保证原子性。
volatile 不能保证 i++ 这样的操作是原子的,因为它涉及多个步骤。
volatile 只能保证 long 和 double 变量的读写是原子的,防止它们在 32 位 JVM 上被拆成两步。
JVM 实现可以自由决定是否拆分 long 和 double 的读写,但推荐保证其原子性。
面试官满意地点点头:“你对 volatile 的理解很深刻,恭喜你通过了面试!”
阿康欣喜若狂,这次终于稳了!
后记:如何应对面试中的 volatile 相关问题?牢记 volatile 的作用(可见性+禁止指令重排,但不保证一般意义上的原子性)。
掌握 i++ 不是原子操作,要使用 synchronized 或 AtomicInteger 来保证安全。
记住 volatile 修饰 long 和 double 变量的特殊性,能保证它们的读写是原子的。
理解 JVM 规范对 long 和 double 的处理,大多数现代 JVM 都会默认保证它们的原子性。
END如果你在面试中被问到 volatile,按照这个思路来回答,面试官一定会对你刮目相看!
如果你觉得这篇文章有帮助,记得点赞、分享、收藏!
我是小米,一个喜欢分享技术的31岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!