刚学多线程,请问下面这段程序为什么停不下来啊?

2021-04-03 21:14:26 +08:00
 Frankhong

书上说的不是很清楚,只说是编译器优化的结果。具体是什么原因呢?编译器再怎么优化 ready=true 不还得执行吗,怎么 while(!ready)就退出不了了呢?

public class NoVisibility {
    private static boolean ready;  
    private static int number;  
  
    private static class ReaderThread extends Thread {  
        public void run() {  
            while (!ready);
            System.out.println(number);  
        }  
    } 
  
    public static void main(String[] args) throws InterruptedException {  
        new ReaderThread().start();  
        Thread.sleep(1000);
        number = 42;  
        ready = true;  
        Thread.sleep(10000);
    }  
}
6240 次点击
所在节点    Java
41 条回复
francis59
2021-04-04 00:33:46 +08:00
-Xint
Operate in interpreted-only mode. Compilation to native code is disabled, and all bytecodes are executed by the interpreter. The performance benefits offered by the Java HotSpot Client VM's adaptive compiler will not be present in this mode.
iseki
2021-04-04 01:28:26 +08:00
你在那个 while 里 println 或者干点啥,多半就发现 ready 起作用了,所以我觉得不是 jit 优化了 ready,单纯缓存的问题
lewis89
2021-04-04 01:35:40 +08:00
@iseki 激进编译的问题,我已经研究过了
lewis89
2021-04-04 01:41:57 +08:00
是虚拟机的激进编译的问题,虚拟机编译的时候 认为 ready 是一个非 volatile 的 static 变量,

激进编译后的汇编代码就只从堆内存里面取了一次这个变量的值放到寄存器里面,
然后寄存器的值不会再被更新,所以就造成了死循环

具体参考这个 https://github.com/jonwinters/jmm-research 的第一节部分,有详细的汇编代码解释
lewis89
2021-04-04 01:51:03 +08:00
@ignor
@wwqgtxx
@az467

反汇编看一下就知道是虚拟机 JIT 的问题,
这种问题本来就是匪夷所思的,对于 static 非 volatile 变量,JVM 编译成本地代码的时候偷懒了,
JVM 认为 static 非 volatile 变量不值得 反复通过内存寻址到堆内存去读取它的值,
所以只读取了一次 放到 CPU 的寄存器里面就完事了,这样 CPU 永远观测不到最新的值,
在某些低版本的 JDK8 或者 ARM 平台 这个代码是可以退出的

具体参考我写的这篇文章 jmm-research
里面有详细的反汇编代码
lewis89
2021-04-04 01:51:48 +08:00
OliverDD
2021-04-04 08:29:19 +08:00
这是内存一致性问题,解决办法好几个,原子变量、volatile 、甚至同步策略也可
bugmakerxs
2021-04-04 08:35:02 +08:00
直接看.class 命令看看,我记得能看到的
BBCCBB
2021-04-04 08:53:00 +08:00
看 jvm 内存模型和 volatile 做什么的就知道了.

每个线程都有自己的工作内存.. 不加 volatile, 变量被缓存在线程的工作内存中,

其他线程写入后也感知不到.
WngShhng
2021-04-04 11:49:50 +08:00
WngShhng
2021-04-04 11:52:19 +08:00
@WngShhng
Reports on while loops which spin on the value of a non-volatile field, waiting for it to be changed by another thread.
In addition to being potentially extremely CPU intensive when little work is done inside the loop, such loops are likely have different semantics than intended, as the Java Memory Model allows such field accesses to be hoisted out of the loop, causing the loop to never complete even if another thread does change the field's value.
Additionally since Java 9 it's recommended to call Thread.onSpinWait() inside spin loop on a volatile field which may significantly improve performance on some hardware.
Martens
2021-04-04 18:47:05 +08:00
ready 加上 volatile 修饰就行了, 保证 ready 变量的线程可见性......
johnniang
2021-04-05 07:51:22 +08:00
反编译一下就知道了。
FrankHB
2021-04-05 13:02:42 +08:00
怎么随便进一个楼都是各种不到点上的……
根本原因是 Java 语言不保证这里的调度公平性,所以明确允许把你 = true 饿死了不管。
这玩意儿跟可见性看起来表面上是有点关系,但不巧,Java 里这条恰恰是单独拎出来授权的:

https://docs.oracle.com/javase/specs/jls/se16/html/jls-17.html#jls-17.4.9

Programs can hang if all threads are blocked or if the program can perform an unbounded number of actions without performing any external actions.

这样设计的理由,在 JSR-133 Chapter 13 里有提,是个妥协:

https://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf

(老实说这样设计挺欠扁的,虽然还有人居然认为这个“容易理解”: https://stefansf.de/post/non-termination-considered-harmful/ ←注意这人说的 C 和 C++ 在这里 undefined behavior 这个明确是错的。)

@az467 你确定这问题真经?

和你说的恰恰相反,除了 JVM 自己高兴是不是愿意这样搞,这跟 JVM 没有半点关系。
关掉 JIT 也不保证一定顶用。

@ipwx C++ 和这个明显不一样。

C++ 是明确允许 forward prograss,反面允许把整个 while 直接优化没掉而不是 blocking:

C++17 § 4.7.2 Forward progress ¶ 1

The implementation may assume that any thread will eventually do one of the following:

- terminate,
- make a call to a library I/O function,
- perform an access through a volatile glvalue, or
- perform a synchronization operation or an atomic operation.

[Note: This is intended to allow compiler transformations such as removal of empty loops, even when termination cannot be proven. — end note]

虽然只是允许,所以实现上可能刚好一样……

另外注意 C++ 这里是每线程内的约束。Java 的 volatile 自 JDK 1.4 后跟 C++11 的 std::atomic 语义类似,但这里 C++ 可是 volatile 就够了。
FrankHB
2021-04-05 13:10:44 +08:00
(当然 C++ 的 volatile 就够了只是说看起来的死循环不被优化没掉,多线程还是该 atomic 就 atomic 该加锁就加锁,否则 data race 了死得更难看。)
ipwx
2021-04-05 14:18:18 +08:00
@FrankHB 嘛嘛,我提一嘴 C++,主要是吐个嘈。我几乎不懂 Java 。
----

C++ 的 atomic 要保证的比 empty removal 要多了去了。譬如编译器或者 cpu 可以 out-of-order execution,可以在读取了某个变量以后假设它不变所以不读第二遍 etc 。。。。 后面那种情况用 volatile 还行,Out-of-order execution 用 volatile 就会死的很难看。。。这也是为啥我在写多线程程序从来不用 volatile (反正没卵用)
ipwx
2021-04-05 14:18:58 +08:00
... 用 atomic 的 memory fence 就能很好解决 out-of-execution 问题。
az467
2021-04-05 15:50:47 +08:00
@FrankHB

啊?

Chapter 13 的例子中程序有加锁,并且没有 external action,所以用 fairness 解释为什么程序可能会被优化成死循环。

原 po 的代码里甚至都没有出现过 synchronized/volatile,任何 happens before 关系都没有,
就算 Java 保证调度公平,难道 JIT 编译器就不优化了?
FrankHB
2021-04-06 03:54:29 +08:00
@az467 原问题是“说的不清楚……具体是什么原因呢?”光看这里,你回答一个具体实现的表现倒也算是对路。
但后面的疑问很明显是为什么允许这样做。拿实现行为就没解决问题。
关键是什么允许,直接的答案就是 JLS 里的规则。(事实上这段 JSR133 里也有。)
虽然跟你之后说的一样,最好看看整个 JMM,但直接原因是 JLS 就确定的,到这里跟 JVM 还不需要有关系。
(可能这里是该强调一下所谓编译器包括 JVM 的 JIT 而不只是 javac 。不过没看出一开始这里是不是有疑问。)
拿 JSR133 主要是说明背景。具体来说,用到的是 Chapter 13 里的第一段文字描述。
Figure 27 是一个典型的例子,有了 synchronize 都允许不让出 CPU,比这里的条件还强。(另外别忘了 happens before 在线程内就有效,不过这个和这里的问题没直接关系。)
(后面那个 Figure 28 例子是反过来限制优化的,和这里也不直接相关。)
重新回顾重点:不是“优化”(而是为什么允许优化)。实际上考虑这里仍然不需要关心“编译器怎么优化”。说白了,编译器就算不是以优化的目的变换代码,直接编译成这样按规范也没话说。
没讲清楚这条线,讲实现细节只会更混乱。
用 volatile 等避免问题也是之后的延伸话题。
(道理上 happens before 也该弄明白,但尴尬的是这个只用线程内的就能直接踩上调度问题了,又不像 C/C++ 直接有 sequence before,还不如默认知道 program order 不言自明了……)
mdkml
2021-04-06 12:06:58 +08:00
@az467 #13

如果把 run 方法替换成下面这种

public void run() {

while (!ready) {
System.out.println(ready);
System.out.println(number);
System.out.println();
}
System.out.println(ready);
System.out.println(number);
System.out.println();
}

就又可以停下了,你说的‘甚至可以反过来“保证”变量完全不可见。’好像不太对呀。

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

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

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

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

© 2021 V2EX