Volatile 的主要作用
保证内存可见性
保证顺序性(禁止指令重排序)
Volatile 是如何生效的
我们从 JAVA 代码从编译到执行这样一个流程去了解下 volatile 是如何实现的。
现在假设我们有如下的一段 JAVA 代码:
package com.kyotom.learn;
public class Example {
private volatile int count; // volatile字段
public void setCount() {
count = 1; // 设置count为1
}
public int getCount() {
return count; // 设置count为1
}
}字节码层面
首先通过 javac 将 java 文件编译成 java 字节码,获得如下的字节码:
警告: 二进制文件Example包含com.kyotom.learn.Example
Classfile /Users/kyotom/Program/idea/projects/learn/src/main/java/com/kyotom/learn/Example.class
Last modified 2025-7-20; size 354 bytes
MD5 checksum 005a28d857685e54fce32c39d00944b2
Compiled from "Example.java"
public class com.kyotom.learn.Example
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#16 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#17 // com/kyotom/learn/Example.count:I
#3 = Class #18 // com/kyotom/learn/Example
#4 = Class #19 // java/lang/Object
#5 = Utf8 count
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 setCount
#12 = Utf8 getCount
#13 = Utf8 ()I
#14 = Utf8 SourceFile
#15 = Utf8 Example.java
#16 = NameAndType #7:#8 // "<init>":()V
#17 = NameAndType #5:#6 // count:I
#18 = Utf8 com/kyotom/learn/Example
#19 = Utf8 java/lang/Object
{
private volatile int count;
descriptor: I
flags: ACC_PRIVATE, ACC_VOLATILE
public com.kyotom.learn.Example();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public void setCount();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: iconst_1
2: putfield #2 // Field count:I
5: return
LineNumberTable:
line 7: 0
line 8: 5
public int getCount();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field count:I
4: ireturn
LineNumberTable:
line 11: 0
}
SourceFile: "Example.java"从字节码文件可以看出对应变量的 flags 中有一个 ACC_VOLATILE。
JVM 层面
JVM 执行字节码分为解释执行和 JIT 编译执行。下面从两个方式分别描述下 JVM 在执行字节码时遇到 ACC_VOLATILE 的变量会怎么执行。
解释执行
JVM 会进行运行时字段访问检查,当访问到有 ACC_VOLATILE 标识的则会在调用对应的字段访问函数前后添加对应内存屏障。
比如执行 putfield 字节码时,其伪代码如下:
// 伪代码:解释器执行putfield指令
void interpret_putfield(u2 field_index) {
Field* field = resolve_field(field_index);
if (field->is_volatile()) {
// volatile字段的特殊处理
memory_barrier_before_store();
perform_volatile_store(field, value);
memory_barrier_after_store();
} else {
// 普通字段的处理
perform_normal_store(field, value);
}
}解释器中的内存屏障:
调用JVM运行时的内存屏障函数
这些函数最终会调用操作系统或CPU的内存屏障原语
void memory_barrier_before_store() {
// 等效于StoreStore屏障
__asm__ __volatile__("mfence" ::: "memory"); // x86
// 或者调用pthread_barrier_wait()等同步原语
}
void memory_barrier_after_store() {
// 等效于StoreLoad屏障
__asm__ __volatile__("mfence" ::: "memory"); // x86
}JIT 编译器执行
JIT编译器扫描字节码时,会解析字段引用的常量池信息
当遇到putfield/getfield指令时,检查目标字段的access_flags
如果发现0x0040(ACC_VOLATILE)标志,标记为volatile访问
中间表示(IR)构建:
普通字段访问:
getfield #2 → IR: LOAD_FIELD
putfield #2 → IR: STORE_FIELD
volatile字段访问:
getfield #2(volatile) → IR: VOLATILE_LOAD_FIELD + 内存屏障标记
putfield #2(volatile) → IR: VOLATILE_STORE_FIELD + 内存屏障标记然后会生成最终的各个平台的代码:
x86平台:生成MFENCE指令
ARM平台:生成DMB ST + DMB SY指令
其他平台:生成对应的屏障指令
CPU 层面
保证内存可见性
同时内存屏障指令会触发缓存一致性协议(MESI)的特定行为,确保缓存一致性操作按照预期的顺序和时机进行。当某个 CPU 将数据写入到缓存前,需要确保之前的写操作触发MESI状态转换完成,然后再写入 volatile 数据,最后还需要等待所有CPU确认收到一致性消息。保证 volatile 的内存的可见性语义。
保证顺序性
CPU 执行的过程分为了 “取指” → “解码” → “执行” → “访存” → “写回” 这五个阶段。我们知道现代 CPU 能通过流水线技术并行处理多条指令:
时钟周期: 1 2 3 4 5 6 7
指令1: 取指 解码 执行 写回
指令2: 取指 解码 执行 写回
指令3: 取指 解码 执行 写回
指令4: 取指 解码 执行 写回
CPU 会通过乱序执行对指令流进行优化。但是内存屏障能够限制这种优化。
CPU 识别出内存屏障指令(如x86的mfence、ARM的dmb),将屏障指令标记为特殊的同步指令,因此不会像普通指令一样进行乱序执行。这个特性保证了 volatile 关键字的保证顺序性,确保 volatile 访问的 happens-before 语义。