为啥线程读取了一个 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;
}
1
vk42 2020-10-05 11:26:54 +08:00
这没什么问题啊,没有 volatile 只是不保证每次引用都会实际取值,又不是说肯定不会取值啊
|
2
mtmax OP |
3
momocraft 2020-10-05 11:44:40 +08:00
感覺是沒有明文保證的行爲
如果主線程寫了 b,新線程讀到 b 時應保證讀到 (所有 happens-before (寫 b 的那次操作) 的寫結果)。但是這裏又沒有寫 b 。 |
4
momocraft 2020-10-05 11:46:10 +08:00
可見性是有形式定義的
實驗+猜屬於 cargo cult,不如先看標準 |
5
vk42 2020-10-05 11:49:32 +08:00
@mtmax volatile 啥时候有可见性的意思? volatile 就是字面意思说明变量值“易变”,一般就是会被硬件或其它线程修改的变量。不给 a 加 volatile 的时候你说的两种情况都没啥问题,完全取决于编译器怎么处理
|
6
littlewing 2020-10-05 11:59:16 +08:00
@vk42
java 的 volatile 使用了内存屏障,确实有可见性的语义 c/c++的 volatile 和 java 的语义不一样,只保证不被编译优化、指令重排和寄存器缓存,可见性和原子性不保证 |
7
littlewing 2020-10-05 12:00:07 +08:00
关键字,java 、volatile 、memory barrier
|
8
sagaxu 2020-10-05 12:06:07 +08:00 via Android
干扰因素很多,
1. System.out 内部加锁,自带线程同步 2. Thread.sleep 等线程方法会不会也隐含同步? 3. a=1 之后线程退出,有没有可能引起同步? @vk42 volatile 在 jvm 里有可见性保证 |
9
vk42 2020-10-05 12:18:12 +08:00
@littlewing @sagaxu 我对 JVM 内存模型确实不太了解,不过这个问题和原子性和 barrier 并没有关系。但 lz 的问题在于理解 volatile 保证可见性,不代表没有 volatile 变量就没有了可见性,应该说不加 volatile 的时候行为是不可确定的
|
10
mtmax OP @sagaxu
1.System.out 前就已经读到 a=1 退出 while 循环了 2.sleep 似乎没有可见性的保证, 就算有, 那么注释掉 long x = b 这行, 线程也应该退出 while 循环, 但实际上注释掉后就无法退出 while 循环 3.同 2 我觉得问题可能就在读 b 这行代码上, 具体不太清楚... |
11
mtmax OP 怀疑是内存屏障的原因
|
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). |
13
iseki 2020-10-05 12:29:33 +08:00
首先 volatile 的保证是单方面的,保证加上能读到最新值,不保证不加上就一定读不到最新值。
至于出现这个现象的原因可能是 volatile 用了内存屏障,这玩意儿会影响的粒度比较大,牵扯上了。 |
14
iseki 2020-10-05 12:31:48 +08:00
所以说这个问题其实牵扯到 JVM 底层对 volatile 的实现,属于规范以外的实现细节(不要面向这种东西编程
|
16
az467 2020-10-05 13:02:11 +08:00
> 如果注释这行, a 就读不到。
这简单,你把 JIT 关掉就行了(如果你也是 open JDK )。 估计是 JVM 直接帮你把 while ( a == 0 )替换成 while ( 0 == 0 )或者 while ( true )了。 所以说这跟可见性根本就没有关系,只跟 JVM 的具体实现有关。 |
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 导致的吗 |
18
Wicked 2020-10-05 16:08:10 +08:00
建议先了解一下 指令乱序,内存屏障,store release,load acquire 等基础概念,然后再去看手册
否则还是老老实实用更高层的同步机制吧,如果不是性能瓶颈,lock 就足够了 |
19
zhgg0 2020-10-05 20:59:34 +08:00
while (a == 0) {
long x = b; // 为什么这里读 b, 能让线程同时读到 a 的最新值? 如果注释这行, a 就读不到 } |
20
zhgg0 2020-10-05 21:02:50 +08:00
while (a == 0) {
long x = b; // 为什么这里读 b, 能让线程同时读到 a 的最新值? 如果注释这行, a 就读不到 } 没有 long x = b; 这行的话,jvm 会优化这几句代码,可能根本就不执行这个死循环。 |
21
Weixiao0725 2020-10-06 07:37:50 +08:00
因为 a 只是普通变量,什么时候刷新到内存要看运行时。当你加上读取 b 的那句代码时,因为 b 是 volatile 的,会强制从内存读,所以这时候强制把 a 的内容重新刷到了内存中,所以这时候就可以读取到最新的 a 值了
|
22
matt5ttam 2020-10-06 11:10:03 +08:00 via iPhone
这个是缓存一致性协议造成的 volatile 会使用 lock#锁总线
|
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; 想必也是考虑到这个方向了吧....而且看这个样子应该也不是缓存行的问题了. |
24
letianqiu 2020-10-06 16:34:24 +08:00
|