关于 volatile 可见性的一个问题

2020-10-05 11:19:04 +08:00
 mtmax

为啥线程读取了一个 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;
}
3161 次点击
所在节点    Java
25 条回复
vk42
2020-10-05 11:26:54 +08:00
这没什么问题啊,没有 volatile 只是不保证每次引用都会实际取值,又不是说肯定不会取值啊
mtmax
2020-10-05 11:37:54 +08:00
@vk42 但是注释读 b 的那行后, 就没法读到 a 的最新值了
我的理解是无法读取 a 的最新值是正确的, 因为 a 没有可见性
但是问题是读 b 后, a 似乎具备了可见性, 这很奇怪
momocraft
2020-10-05 11:44:40 +08:00
感覺是沒有明文保證的行爲

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

@vk42 volatile 在 jvm 里有可见性保证
vk42
2020-10-05 12:18:12 +08:00
@littlewing @sagaxu 我对 JVM 内存模型确实不太了解,不过这个问题和原子性和 barrier 并没有关系。但 lz 的问题在于理解 volatile 保证可见性,不代表没有 volatile 变量就没有了可见性,应该说不加 volatile 的时候行为是不可确定的
mtmax
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
2020-10-05 12:19:59 +08:00
怀疑是内存屏障的原因
sagaxu
2020-10-05 12:20:44 +08:00
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
2020-10-05 12:29:33 +08:00
首先 volatile 的保证是单方面的,保证加上能读到最新值,不保证不加上就一定读不到最新值。
至于出现这个现象的原因可能是 volatile 用了内存屏障,这玩意儿会影响的粒度比较大,牵扯上了。
iseki
2020-10-05 12:31:48 +08:00
所以说这个问题其实牵扯到 JVM 底层对 volatile 的实现,属于规范以外的实现细节(不要面向这种东西编程
sagaxu
2020-10-05 12:38:55 +08:00
@mtmax 试试在 a=1 之后加一行
Thread.sleep(1000)
az467
2020-10-05 13:02:11 +08:00
> 如果注释这行, a 就读不到。

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

所以说这跟可见性根本就没有关系,只跟 JVM 的具体实现有关。
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
2020-10-05 16:08:10 +08:00
建议先了解一下 指令乱序,内存屏障,store release,load acquire 等基础概念,然后再去看手册

否则还是老老实实用更高层的同步机制吧,如果不是性能瓶颈,lock 就足够了
zhgg0
2020-10-05 20:59:34 +08:00
while (a == 0) {
long x = b; // 为什么这里读 b, 能让线程同时读到 a 的最新值? 如果注释这行, a 就读不到
}
zhgg0
2020-10-05 21:02:50 +08:00
while (a == 0) {
long x = b; // 为什么这里读 b, 能让线程同时读到 a 的最新值? 如果注释这行, a 就读不到
}
没有 long x = b; 这行的话,jvm 会优化这几句代码,可能根本就不执行这个死循环。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/712592

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX