为什么volatile不能保证i++的原子性?但对long和double却是例外?

软件求生 2025-03-05 08:57:15



大家好,我是你们的技术小伙伴小米!今天我们来聊一个 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岁程序员。如果你喜欢我的文章,欢迎关注我的微信公众号“软件求生”,获取更多技术干货!

0 阅读:0
软件求生

软件求生

从事软件开发,分享“技术”、“运营”、“产品”等。