kyo-tom
发布于 2025-07-20 / 20 阅读
0
0

探索 Volatile 关键字

Volatile 的主要作用

  1. 保证内存可见性

  2. 保证顺序性(禁止指令重排序)

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 语义。

参考文档

  1. 一次深入骨髓的 volatile 研究


评论