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

Java Happens-before 的疑问

  •  
  •   chanlk · 2022-05-07 17:23:26 +08:00 · 1820 次点击
    这是一个创建于 691 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在网上看电子书,里面写的 JVM 的 Happens-before 示例感觉有些疑惑,怀疑是写错了 有熟悉的老哥帮忙看下是写错了还是我理解错了

    15 条回复    2022-05-22 23:18:45 +08:00
    chanlk
        1
    chanlk  
    OP
       2022-05-07 17:24:30 +08:00
    是两个截图,格式没弄好 (⊙︿⊙)
    alen0206
        2
    alen0206  
       2022-05-07 17:33:57 +08:00
    Happens-Before 的语义本质上是一种可见性,A Happens-Before B 意味着 A 事件对 B 事件来说是可见的,无论 A 事件和 B 事件是否发生在同一个线程里。例如 A 事件发生在线程 1 上,B 事件发生在线程 2 上,Happens-Before 规则保证线程 2 上也能看到 A 事件的发生。
    chanlk
        3
    chanlk  
    OP
       2022-05-07 17:36:42 +08:00
    @alen0206 嗯是的是的,截图 1 的那句话是对的,那个是伪代码,加上各种手段使得它满足 Happens-Before ,然后就可以操作 b 执行后,变量 j 的值一定是等于 1 。
    chanlk
        4
    chanlk  
    OP
       2022-05-07 17:39:09 +08:00
    @alen0206 截图 2 的那个 reader 中当 flag==true 的时候,i 不一定是 42 吧; writer 中代码是可以重排序的,a 也没有保证写入 42 后能够立刻被另一个线程可见
    alen0206
        5
    alen0206  
       2022-05-07 17:56:56 +08:00
    @chanlk 程序按顺序执行 也是 happen-before 的规则之一
    alen0206
        6
    alen0206  
       2022-05-07 18:01:24 +08:00
    @chanlk volatile 的内存屏障是有对应禁止重排序规则的
    documentzhangx66
        7
    documentzhangx66  
       2022-05-08 00:46:17 +08:00   ❤️ 1
    Q1:这里的 i 没有 volatile ,线程 b 不能看到吧?

    是的。

    但是,作者那句话,开头有个关键词:“假设”。作者的意思是,已经假设 操作 A 发生在 操作 B 前面了。所以这里有没有 volatile 已经无关紧要。



    Q2:这个是错误的吧?

    是的。这里有两个问题。

    第一个问题:
    Happens Before 原则,只提到同一个线程内的连续上下文,并没有提跨方法、跨线程的问题。

    我怀疑这篇文章的作者,是不是看了这篇英文资料:
    https://www.geeksforgeeks.org/happens-before-relationship-in-java/

    这篇资料里 class ClassRoom 下面有两个方法,需要用不同线程,去同时执行。如果单独看任意一个方法的内部代码与执行顺序,这篇资料讨论的结论是对的。但,如果同时看两个线程、两个方法的并行关系,这篇资料的结论就完全错了,错误原因,涉及到太多东西,过于复杂,就不展开了,但我还是举个简单的反例,Thread 1 对应的方法 submitAssignment( ) ,内部 this.assgn = assgn; 这行代码,对 this.assgn 进行改变,直到方法结束后,这次改变的 new value ,是有可能,被一直保留在线程所在 CPU 的 Cache 或 当前 Thread Stack 内部,没有改动到 HEAP 的位置,Thread 2 自然就读不到该变量的 new value 。

    问题 2:
    这篇英文资料,说的是两个线程,同时执行。但楼主发的这篇中文资料,文章作者偷偷地作弊了,仔细看,他写的是:

    线程 A 执行 writer( ) 方法之后,然后线程 B 才去执行 reader( ) 方法....

    关键词:之后。

    大多数正常情况下,writer( ) 执行完毕后,a 与 flag 的 new Value 都已经被 flush 到 HEAP 了,这个例子与 Happens Before 还有毛线关系?作者这例子明显是在坑小白。

    不过也要注意到,就像在问题 1 中,我举得那个例子一样。特殊情况下,writer( ) 执行完毕后,a 与 flag 的 new Value ,是有可能,被一直保留在线程所在 CPU 的 Cache 或 Thread Stack 内部,没有改动到 HEAP 的位置,因此后续线程 B 执行的 reader( )方法里,就读不到这两个变量的 new value 。



    建议:
    这个问题的本质,涉及到太多知识。如果楼主想弄清楚,建议学习:

    A.CPU 在执行代码与同步方面的知识。

    B.汇编在同步方面的知识。

    C.C++是如何处理这个问题的。
    nlzy
        8
    nlzy  
       2022-05-08 07:26:21 +08:00
    直接给结论:如果语句 4 执行了,4 一定能保证读到 a 的值是 42 。A 和 B 线程的执行顺序不影响这个结论。

    反对楼上说法的说法:“Happens Before 原则,只提到同一个线程内的连续上下文,并没有提跨方法、跨线程的问题。”

    Java 8 语言规范 Threads and Locks 那一章对 Happens-before Order 的第一句介绍偏偏就是:“Two actions can be ordered by a happens-before relationship.”。而 action 一词在那章规范中就专指跨线程:“We will usually refer to inter-thread actions more succinctly as simply actions.”
    chanlk
        9
    chanlk  
    OP
       2022-05-08 12:26:50 +08:00
    @documentzhangx66 @nlzy 感谢回答~
    问题一那个应该是伪代码哈, 然后是先做了 Happens-before 的假设再说行为, 是对的;
    问题二那个确实是错的,我直接跑了下代码,确实会出现 flag==true 的时候 a!=42 的情况
    nlzy
        10
    nlzy  
       2022-05-08 14:25:01 +08:00 via Android
    @chanlk 上完整代码
    documentzhangx66
        11
    documentzhangx66  
       2022-05-08 16:26:47 +08:00
    @nlzy

    我的原话是:
    Happens Before 原则,只提到同一个线程内的连续上下文,并没有提跨方法、跨线程的问题。

    意思是,Happens Before 原则 并没有给出跨方法、跨线程的定义与规则。


    你提到的规范:
    Two actions can be ordered by a happens-before relationship.

    这里并没有专门对跨方法和跨线程进行定义与规则了?

    注意关键字:can 。can ≠ must 。

    意思是,你可以通过一些特殊手段,强行让两个 Actions 实现 happens-before ,否则,任意两个没有被特殊照顾的 Actions 不一定会遵守 happens-before ,这结论,楼主在 9 楼已经验证出来了。

    另外,你知道为什么 Java 规范要专门搞这么一个玩意嘛:We will usually refer to inter-thread actions more succinctly as simply actions. 猜猜看?
    heiher
        12
    heiher  
       2022-05-09 09:04:41 +08:00 via Android
    @chanlk 示例 2 不管是 writer 还是 reader 先执行,只要 flag==true ,则 i 必然==42 吧。因为 flag 是 volatile 变量,不仅限制了编译器代码生成上其它变量访存与 flag 访存的重排,在弱内存一致性架构上还会插入内存屏障指令阻止硬件执行时的乱序。
    az467
        13
    az467  
       2022-05-10 12:16:44 +08:00
    我说啊,这东西,不就是 JSR-133 作者之一给出的半官方例子的变体嘛……

    要么 4 不执行,要么 i == 42 。



    http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html
    chanlk
        14
    chanlk  
    OP
       2022-05-10 14:22:14 +08:00
    @az467 @nlzy @documentzhangx66

    关于第二个问题:结论是对的,是我弄错了,sorry ;
    我前面说能复现的代码是错的,代码如下:

    class Scratch {

    int a = 0;
    boolean flag = false;

    public static void main(String[] args) {

    final Scratch scratch = new Scratch();
    new Thread(() -> {
    while (true) {
    scratch.reader();
    }
    }).start();

    new Thread(() -> {
    while (true) {
    scratch.writer();
    }
    }).start();
    }

    public void writer() {
    flag = false;
    a = 41;
    a = 42;
    flag = true;
    }

    public void reader() {
    if (flag) {
    if (a != 42) {
    System.out.println("error!");
    }
    }
    }
    }

    这里我搞错了,这里能打印出 a!=42 明显是线程切换导致的, 不能画蛇添足在上面再次为 a 赋值的。

    一个值得提醒的现象:
    示例代码中即使把 flag 的 volatile 去掉,a 的值也无法复现出 0 的情况(jdk7/jdk8 均不行),查资料说 x86cpu 不支持写重排序,x86cpu 的市场占有率那么高,所以大部分同学都无法复现,mac m1 的同学可以试试能不能复现

    我的想法和 documentzhangx66 同学的是一样的,但是 volatile 的能力其实很强的,az467 发的那个文章写的很清楚了

    最后,个人愚见,如 volatile 那么底层的工具,没有特殊的需求还是尽量少用,用更加上层的工具在开发和后续的维护上都更合适些
    giiiiiithub
        15
    giiiiiithub  
       2022-05-22 23:18:45 +08:00
    人家两个说的是“假设”啊,有啥问题?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   3166 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 12:22 · PVG 20:22 · LAX 05:22 · JFK 08:22
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.