• 你真的理解 volatile 关键字了吗?


    你真的理解 volatile 关键字了吗?

    volatile 不只是"线程可见"。这篇文章用几个具体的 case,把它在 JMM 里的工作方式说清楚。


    一、从最简单的 volatile 开始

    volatile int flag = 0;
    
    // 线程 A
    flag = 1;
    
    // 线程 B
    if (flag == 1) {
        // ...
    }
    

    volatile 做了什么?

    建立 happens-before:

    • 线程 A 对 flag 的写 happens-before 线程 B 对 flag 的读

    还有一条容易忽略的规则:

    volatile 写之前的所有普通写,在 volatile 读之后对读线程可见


    二、volatile 不是"广播",而是"同步点"

    一个常见的误解是:只要加了 volatile,其他线程就会自动看到变化。

    这是错的。

    Case 1:写了 volatile,但没读

    int A = 0;
    volatile int B = 0;
    
    // 线程 A
    A = 1;
    B = 1;
    
    // 线程 B
    int x = A;   // 能看到 1 吗?
    

    结论:❌ 不能保证

    volatile 的 happens-before 只在"读发生时"生效。线程 B 没有读 B,没有同步点,就没有可见性保证。

    volatile 是拉模型,不是推模型。


    三、volatile + 普通变量的正确发布模式

    Case 2:volatile 作用的传递性

    线程 B 能正确读到 A 吗?

    int A = 0;
    volatile boolean ready = false;
    
    // 线程 A
    A = 1;
    ready = true;
    
    // 线程 B
    if (ready) {
        System.out.println(A);
    }
    

    结论:✅ A 一定是 1

    原因是 happens-before 的传递性:

    如果 A happens-before B,B happens-before C
    那么 A happens-before C

    链路完整:

    线程 a:
    A = 1
      ↓  (线程内顺序)
    B = 1  (volatile 写)
      ↓  (volatile 写-读)
    线程 b:
    if (B == 1)
      ↓  (传递性)
    读取 A == 1
    

    这是 JMM 官方认可的发布-订阅模式:

    // producer
    data = ...; // 普通写
    ready = true; // volatile 写
    
    // consumer
    if (ready) { // volatile 读
        use(data); // 安全
    }
    

    四、多个 volatile 变量 ≠ 自动传递

    Case 3:A 普通,B / C 是 volatile

    int A = 0;
    volatile int B = 0;
    volatile int C = 0;
    
    // 线程 A
    A = 1;
    B = 1;
    
    // 线程 B
    int c = C;
    int a = A;
    

    结论:❌ 不能保证看到 A = 1

    volatile 的 happens-before 只对同一个 volatile 变量生效:

    • 写 B → 读 B:有保证
    • 写 B → 读 C:没有任何语义关系

    volatile 不会跨变量广播内存可见性。


    五、对象字段里有 volatile,就能"顺带同步"吗?

    Case 4:对象里 ABC,只有 C 是 volatile

    class Obj {
        int A;
        int B;
        volatile int C;
    }
    
    // 线程 A
    obj.A = 1;
    obj.B = 2;
    obj.C = 3;
    
    // 线程 B
    int a = obj.A;
    int b = obj.B;
    

    结论:❌ 不能保证

    volatile 是变量级别的,不是对象级别的。不读 C,就不会触发 volatile 读,A / B 仍然是普通读。


    六、"我读了 volatile,但没用它",算吗?

    Case 5:返回 volatile,但返回值没用

    volatile int C;
    
    int foo() {
        return C;
    }
    
    // 线程 B
    foo();  // 返回值未使用
    

    结论:⚠️ 不一定算

    语义上确实是一次 volatile 读,但如果返回值没被使用,JIT 有权把这次读整个消掉。

    两个层面分开看:

    ① Java 语言语义层面

    return C; 在规范里就是一条 volatile 读指令,没有争议。

    ② JVM / JIT 实际执行层面

    JMM 允许 JVM 这么做:

    如果一个 volatile 读的结果对程序行为没有任何影响,JVM 可以消除这个读。

    这个返回值没有赋给变量、没有用于条件判断、没有参与计算,对可观察行为没有影响,JVM 会把它当死代码处理。

    需要澄清一点:

    volatile 的内存语义,不是 Java 语言层面的可观察副作用。

    volatile 只约束重排序与可见性,不算 I/O、不算异常、不算锁,除非它被实际执行,否则不是 happens-before 的强制保留点。


    七、什么时候 volatile 读一定成立?

    Case 6:volatile 参与了判断

    volatile int C;
    
    boolean foo() {
        return C == 0;
    }
    

    结论:✅ 一定是 volatile 读,不可消除

    return C == 0; 和上面的 return C; 区别在于:这里读取 C 的结果直接参与了比较,影响了方法返回值。

    方法返回值是可观察行为,JVM 没有任何空间把它优化掉。


    八、volatile 的本质

    可以把 volatile 理解为:

    • 写 volatile = release
    • 读 volatile = acquire

    它同步的不是"某个变量",而是一个时间点:

    在这个点之前,数据已经写完了


    九、volatile 在 Disruptor 中的应用

    用 Disruptor 来验证一下这个模型。

    很多人用 Disruptor 时有这个疑惑:Event 字段没有 volatile,生产者写完数据,消费者却总能看到最新值。

    原因是:Disruptor 同步的不是数据,而是"数据写完"这件事。

    核心结构

    Event 普通字段
         ↑
    Sequence(volatile

    生产者逻辑(简化)

    Event e = ringBuffer.get(seq);
    e.x = ...;
    e.y = ...;
    sequence.set(seq); // volatile 写(发布)
    

    消费者逻辑

    long s = sequence.get(); // volatile 读
    Event e = ringBuffer.get(s);
    

    happens-before 链

    写 Event 字段
      ↓
    volatile 写 sequence
      ↓ happens-before
    volatile 读 sequence
      ↓
    读 Event 字段
    

    和前面的发布-订阅模式一模一样。

    Event 字段不需要 volatile,因为它不是并发访问,而是被发布的快照。一个 event slot 只被一个生产者写,之后只被一个消费者读,靠 Sequence 做交接。这是所有权转移,不是共享访问。

    如果 Event 字段都是 volatile:每个字段访问都有内存屏障,cache line 来回抖,false sharing 风险暴涨。

    Disruptor 用一次 volatile 发布整个对象,相当于一次内存批量提交,比字段粒度的 volatile 快得多。


    十、小结

    1. volatile 不是"自动可见",而是同步点
    2. 不读 volatile,就没有 happens-before
    3. volatile 不会跨变量传播可见性
    4. 可能被优化掉的 volatile 读,不是同步点
    5. 高性能无锁结构,都是"数据 + volatile 发布点"

    写在最后

    volatile 在 JMM 里的行为比大多数人理解的要复杂。不是"加了就有可见性",而是"读的那一刻建立了同步"。把这个模型理解清楚,写并发代码时才能真正知道自己在保证什么。

  • 相关阅读:
    如何展开一个专题分析报告
    【数据结构-图】图的常用算法
    springboot农机电招平台springboot37
    Windows 11 新材质 Mica Alt 效果展示
    谷歌怒怼 iMessage 锁定 iPhone 用户,安卓用户受到同侪欺凌
    9.1 运用API创建多线程
    一个合格的高级开发,首先应该写的一手好注释。
    转行软件测试,薪资10K | 真正明智的人,懂得脚踏实地 ...
    Java中 常见的开源树库介绍
    C语言—详解库函数中常用的字符串函数
  • 原文地址:https://www.cnblogs.com/wsss/p/20038570