V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
mtmax
V2EX  ›  Java

关于 volatile 可见性的一个问题

  •  1
     
  •   mtmax · 2020-10-05 11:19:04 +08:00 · 3330 次点击
    这是一个创建于 1508 天前的主题,其中的信息可能已经有所发展或是发生改变。

    为啥线程读取了一个 volatile 变量 b, 居然能同时读到非 volatile 变量 a 的最新值

    static long a = 0;
    
    static long p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16;
    
    static volatile long b = 0;
    
    public static void main(String[] args) throws InterruptedException {
    
        new Thread(() -> {
            while (a == 0) {
                long x = b; // 为什么这里读 b, 能让线程同时读到 a 的最新值?  如果注释这行, a 就读不到
            }
            System.out.println("a=" + a);
        }).start();
    
        Thread.sleep(100);
    
        a = 1;
    }
    
    25 条回复    2020-10-06 16:39:14 +08:00
    vk42
        1
    vk42  
       2020-10-05 11:26:54 +08:00
    这没什么问题啊,没有 volatile 只是不保证每次引用都会实际取值,又不是说肯定不会取值啊
    mtmax
        2
    mtmax  
    OP
       2020-10-05 11:37:54 +08:00
    @vk42 但是注释读 b 的那行后, 就没法读到 a 的最新值了
    我的理解是无法读取 a 的最新值是正确的, 因为 a 没有可见性
    但是问题是读 b 后, a 似乎具备了可见性, 这很奇怪
    momocraft
        3
    momocraft  
       2020-10-05 11:44:40 +08:00
    感覺是沒有明文保證的行爲

    如果主線程寫了 b,新線程讀到 b 時應保證讀到 (所有 happens-before (寫 b 的那次操作) 的寫結果)。但是這裏又沒有寫 b 。
    momocraft
        4
    momocraft  
       2020-10-05 11:46:10 +08:00
    可見性是有形式定義的
    實驗+猜屬於 cargo cult,不如先看標準
    vk42
        5
    vk42  
       2020-10-05 11:49:32 +08:00
    @mtmax volatile 啥时候有可见性的意思? volatile 就是字面意思说明变量值“易变”,一般就是会被硬件或其它线程修改的变量。不给 a 加 volatile 的时候你说的两种情况都没啥问题,完全取决于编译器怎么处理
    littlewing
        6
    littlewing  
       2020-10-05 11:59:16 +08:00
    @vk42
    java 的 volatile 使用了内存屏障,确实有可见性的语义
    c/c++的 volatile 和 java 的语义不一样,只保证不被编译优化、指令重排和寄存器缓存,可见性和原子性不保证
    littlewing
        7
    littlewing  
       2020-10-05 12:00:07 +08:00
    关键字,java 、volatile 、memory barrier
    sagaxu
        8
    sagaxu  
       2020-10-05 12:06:07 +08:00 via Android
    干扰因素很多,
    1. System.out 内部加锁,自带线程同步
    2. Thread.sleep 等线程方法会不会也隐含同步?
    3. a=1 之后线程退出,有没有可能引起同步?

    @vk42 volatile 在 jvm 里有可见性保证
    vk42
        9
    vk42  
       2020-10-05 12:18:12 +08:00
    @littlewing @sagaxu 我对 JVM 内存模型确实不太了解,不过这个问题和原子性和 barrier 并没有关系。但 lz 的问题在于理解 volatile 保证可见性,不代表没有 volatile 变量就没有了可见性,应该说不加 volatile 的时候行为是不可确定的
    mtmax
        10
    mtmax  
    OP
       2020-10-05 12:19:35 +08:00
    @sagaxu
    1.System.out 前就已经读到 a=1 退出 while 循环了
    2.sleep 似乎没有可见性的保证, 就算有, 那么注释掉 long x = b 这行, 线程也应该退出 while 循环, 但实际上注释掉后就无法退出 while 循环
    3.同 2
    我觉得问题可能就在读 b 这行代码上, 具体不太清楚...
    mtmax
        11
    mtmax  
    OP
       2020-10-05 12:19:59 +08:00
    怀疑是内存屏障的原因
    sagaxu
        12
    sagaxu  
       2020-10-05 12:20:44 +08:00 via Android
    Synchronization actions, which are:

    Volatile read. A volatile read of a variable.

    Volatile write. A volatile write of a variable.

    Lock. Locking a monitor

    Unlock. Unlocking a monitor.

    The (synthetic) first and last action of a thread.

    Actions that start a thread or detect that a thread has terminated (§17.4.4).
    iseki
        13
    iseki  
       2020-10-05 12:29:33 +08:00
    首先 volatile 的保证是单方面的,保证加上能读到最新值,不保证不加上就一定读不到最新值。
    至于出现这个现象的原因可能是 volatile 用了内存屏障,这玩意儿会影响的粒度比较大,牵扯上了。
    iseki
        14
    iseki  
       2020-10-05 12:31:48 +08:00
    所以说这个问题其实牵扯到 JVM 底层对 volatile 的实现,属于规范以外的实现细节(不要面向这种东西编程
    sagaxu
        15
    sagaxu  
       2020-10-05 12:38:55 +08:00 via Android
    @mtmax 试试在 a=1 之后加一行
    Thread.sleep(1000)
    az467
        16
    az467  
       2020-10-05 13:02:11 +08:00
    > 如果注释这行, a 就读不到。

    这简单,你把 JIT 关掉就行了(如果你也是 open JDK )。
    估计是 JVM 直接帮你把 while ( a == 0 )替换成 while ( 0 == 0 )或者 while ( true )了。

    所以说这跟可见性根本就没有关系,只跟 JVM 的具体实现有关。
    octobered
        17
    octobered  
       2020-10-05 14:10:12 +08:00
    用 gdb 搞了一下,确实是 @az467 说的这样子的,设置了 -Djava.compiler=NONE 就可以解决了
    具体拿 gdb 反汇编出来是这样的
    0x7f714b23cfec: movabs $0x45044ff28,%r10
    0x7f714b23cff6: mov 0x70(%r10),%r10 // 稍晚时候看,0x45044ff28+0x70 这个位置确实已经是 1 了
    0x7f714b23cffa: test %r10,%r10 // 比较是否为 0 只比了这么一次
    0x7f714b23cffd: jne 0x7f714b23d00b
    0x7f714b23cfff: mov 0x108(%r15),%r10 // 之后都是从$r15+0x108 这个地方读,而这里一直是 0
    => 0x7f714b23d006: test %eax,(%r10)
    0x7f714b23d009: jmp 0x7f714b23cfff
    0x7f714b23d00b: mov $0xffffff7e,%esi

    具体为什么是从$15+0x108 读,有无大佬来解释一下,是 jit 导致的吗
    Wicked
        18
    Wicked  
       2020-10-05 16:08:10 +08:00
    建议先了解一下 指令乱序,内存屏障,store release,load acquire 等基础概念,然后再去看手册

    否则还是老老实实用更高层的同步机制吧,如果不是性能瓶颈,lock 就足够了
    zhgg0
        19
    zhgg0  
       2020-10-05 20:59:34 +08:00
    while (a == 0) {
    long x = b; // 为什么这里读 b, 能让线程同时读到 a 的最新值? 如果注释这行, a 就读不到
    }
    zhgg0
        20
    zhgg0  
       2020-10-05 21:02:50 +08:00
    while (a == 0) {
    long x = b; // 为什么这里读 b, 能让线程同时读到 a 的最新值? 如果注释这行, a 就读不到
    }
    没有 long x = b; 这行的话,jvm 会优化这几句代码,可能根本就不执行这个死循环。
    Weixiao0725
        21
    Weixiao0725  
       2020-10-06 07:37:50 +08:00
    因为 a 只是普通变量,什么时候刷新到内存要看运行时。当你加上读取 b 的那句代码时,因为 b 是 volatile 的,会强制从内存读,所以这时候强制把 a 的内容重新刷到了内存中,所以这时候就可以读取到最新的 a 值了
    matt5ttam
        22
    matt5ttam  
       2020-10-06 11:10:03 +08:00 via iPhone
    这个是缓存一致性协议造成的 volatile 会使用 lock#锁总线
    Newyorkcity
        23
    Newyorkcity  
       2020-10-06 15:22:11 +08:00
    我本来想说会不会是缓存行(字)的问题..

    但楼主的代码里之所以会有

    static long p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16;

    想必也是考虑到这个方向了吧....而且看这个样子应该也不是缓存行的问题了.
    letianqiu
        25
    letianqiu  
       2020-10-06 16:39:14 +08:00
    @Wicked 这么多楼就你一个明白人。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2863 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 14:49 · PVG 22:49 · LAX 06:49 · JFK 09:49
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.