小白问一个 Java 线程 jmm 的问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
liliumss
V2EX    Java

小白问一个 Java 线程 jmm 的问题

  •  1
     
  •   liliumss 2020 年 3 月 6 日 3797 次点击
    这是一个创建于 2240 天前的主题,其中的信息可能已经有所发展或是发生改变。

    小弟正在学 java jmm 知识,做了个实验代码和执行结果如下,启动了 2 个线程来看共享的 count 变量是否被线程共享,按照 jmm 理论 线程应该会加载公共变量到自己的线程,互相不影响,咋执行结果线程 2 赋值后,线程 1 就不++了,往指点,谢谢了

    public class Test { private static Long count = 0L; public static void main(String[] args) throws InterruptedException { new Thread(() -> { System.out.println("Thread1 start"); while (count < 10000L) { count++; System.out.println("Thread1 count:" + count); } System.out.println("Thread1 end"); }).start(); new Thread(() -> { System.out.println("Thread2 start"); count = 10000L; System.out.println("Thread2 count:" + count); System.out.println("Thread2 end"); }).start(); } } 

    执行结果

    Thread1 start Thread1 count:1 Thread2 start Thread1 count:2 Thread1 end Thread2 count:10000 Thread2 end 
    22 条回复    2020-03-08 13:38:20 +08:00
    xmh51
        1
    xmh51  
       2020 年 3 月 6 日
    你定义了一个静态变量。。
    kevincai100
        2
    kevincai100  
       2020 年 3 月 6 日   1
    静态变量在方法区,共享内存,被别人改了立即可见的, 应该不是线程里面的副本
    fihserman123
        3
    fihserman123  
       2020 年 3 月 6 日
    静态变量在方法区( JDK8 后是 meta space )中,线程或者说 run 方法的变量存于栈中。而位于 enclosing class 中的静态变量 private static Long count = 0L; 对于 run 方法而言是可见的,换句话说,你代码里的三个 count 存有同一个 int 数值,且相互影响。你需要做的是在俩线程的 run 方法中分别额外定义一个 count 变量。
    fihserman123
        4
    fihserman123  
       2020 年 3 月 6 日
    静态变量貌似去堆中了(逃。
    sagaxu
        5
    sagaxu  
       2020 年 3 月 6 日 via Android
    jmm 哪个条款说互不影响了?
    liliumss
        6
    liliumss  
    OP
       2020 年 3 月 6 日
    @fihserman123 但是我顶一个静态的 boolean 值确是 2 线程互不影响的 这是什么原理呢
    liliumss
        7
    liliumss  
    OP
       2020 年 3 月 6 日
    @kevincai100 我测试了下 boolean 值确是可以的 第一个线程会一直卡在循环中
    代码如下:
    public class Test {
    private static Boolean flag = false;

    public static void main(String[] args) throws InterruptedException {

    new Thread(() -> {
    System.out.println("Thread3 start");
    while (!flag) {
    }
    }).start();

    Thread.sleep(1000);
    new Thread(() -> {
    flag = true;
    System.out.println("Thread4 end");
    }).start();
    }
    }
    liliumss
        8
    liliumss  
    OP
       2020 年 3 月 6 日
    @fihserman123 用 boolean 值确是可以的 第一个线程一直卡在循环,这是为啥呢
    ```
    public class Test {
    private static Boolean flag = false;

    public static void main(String[] args) throws InterruptedException {

    new Thread(() -> {
    System.out.println("Thread3 start");
    while (!flag) {
    }
    }).start();

    Thread.sleep(1000);
    new Thread(() -> {
    flag = true;
    System.out.println("Thread4 end");
    }).start();
    }
    }



    ```
    Jooooooooo
        9
    Jooooooooo  
       2020 年 3 月 6 日   1
    @liliumss 没有定义 volatile 导致 thread 3 寄存器的里的值一直是旧的. 由于非 volatile 的, 这里 thread 3 跑的那个 cpu 可以无限期的去使用寄存器缓存里面存放的 count 值.(当然这个行为是不定的, 不同机器上表现也不会一致)

    而题目里那种场景, 因为 thread 1 有更新, 等于是和主内存有交互(其实是 L1 cache), 寄存器的值就被更新成最新的了. 一般硬件的 MESI 协议会保证各个 cpu 核上看见的值是一致(大体是这种意思, 更具体的可以搜搜 MESI)
    liliumss
        10
    liliumss  
    OP
       2020 年 3 月 6 日
    @Jooooooooo 谢谢你的回答
    意思就是线程 1 的那个 count++ 触发了 MESI 协议与主内存有交互了,而正好线程 2 把 count 值改变了所以线程 1 就直接满足条件推出了
    而线程 3 一直没更新,又没使用 volatile 保证可见性,所以即使线程 更改了 boolean 值也无法从循环跳出
    我理解的对吧,关键就是线程 1 的 count++导致了 2 种不同变量在后面操作的差别
    az467
        11
    az467  
       2020 年 3 月 7 日
    JMM 和 JVM 的内存结构不是一一对应的关系,或者说他们定义的不是一个层面上的事情,
    所以不要去用堆栈元空间什么的去分析。

    JMM 什么时候刷新缓存到主存,什么时候读取主存,如果不加 volatile,那么都是不一定的。
    你多执行几次就会发现执行结果还可能是这样:

    Thread1 start
    Thread1 count:1
    Thread1 count:2
    Thread1 count:3
    Thread1 count:4
    Thread1 count:5
    Thread1 count:6
    Thread1 count:7
    Thread1 count:8
    Thread1 count:9
    Thread1 count:10
    Thread1 count:11
    Thread1 count:12
    Thread1 count:13
    Thread1 count:14
    Thread1 count:15
    Thread1 count:16
    Thread1 count:17
    Thread1 count:18
    Thread2 start
    Thread1 count:19
    Thread1 end
    Thread2 count:10000
    Thread2 end
    liliumss
        12
    liliumss  
    OP
       2020 年 3 月 7 日 via iPhone
    @az467 我本地也是这个结果 我纳闷是根据 jmm 搜第一个 thread 还在在循环 assgin count 的时候 第二个线程结束同步 write 给主存的值为啥影响了第一个线程的 count 这里并没设置 valiate 修饰 而用 boolan 做的 demo (见楼层)确是可以的
    sagaxu
        13
    sagaxu  
       2020 年 3 月 7 日   1
    两个建议
    1. 测试并发时内存模型的代码,尽量用 jcstress 而不是自己构造。
    2. 不要调用 System.out.println 这样的方法,你怎么知道这个方法没有起到同步的作用?

    事实上,某个 JDK 给这个 println 加了同步语义,两个线程都调用,那就在调用点建立了 happens-before 关系
    public void println(boolean x) {
    synchronized(this) {
    this.print(x);
    this.newLine();
    }
    }
    az467
        14
    az467  
       2020 年 3 月 7 日   1
    @liliumss JMM 并没有规定对非 volatile 变量的修改对其他线程完全不可见,不存在什么“互不影响”。
    我们只能说这是不确定的,在不同的平台上于不同的时间执行会得到多种结果,出现什么情况都不奇怪。

    第一个 demo,如果某 CPU 还没有把修改后的值写入 L1cache,或者 CPU 根本不保证缓存一致性,那么修改还是可能不(立即)可见。

    第二个 demo,Thread4 会无限循环,是因为 JIT 的神奇优化,你把 JIT 关了程序就可以退出了,而 JIT 并不是必须的,各版本的 JIT 也不尽相同,所以还是可能可见。

    java -Xint DemoApplication
    Thread3 start
    Thread4 end
    //然后程序退出

    所以这根本不违背 JMM,没有什么好纳闷的,真要纳闷的话,那些凌乱的底层原理可太多了。

    如果要深究为何如此,那就与 JMM 无关了。
    liliumss
        15
    liliumss  
    OP
       2020 年 3 月 7 日 via iPhone
    @sagaxu 谢谢 请问使用什么方法替代 system.out.print 呢
    liliumss
        16
    liliumss  
    OP
       2020 年 3 月 7 日 via iPhone
    @az467 谢谢 请问看这方面知识有啥好文档呢
    yanyueio
        17
    yanyueio  
       2020 年 3 月 7 日 via Android
    找本靠谱点的 JAVA 语法关键字指北吧,然后再去理解 jvm,jmm,并且一定注意 jdk/jre 实现版本。同楼上,很多优化导致了不确定,具体根据想象以及自己的操作系统功底,具体分析。
    lewis89
        18
    lewis89  
       2020 年 3 月 7 日   1
    多线程研究这些变量之间的一致性,说实话真的没必要,加锁就好了 锁的语义就有 invalid cache 从主内存读 然后 invalid cache 写入主内存的意思
    lewis89
        19
    lewis89  
       2020 年 3 月 7 日
    中间还涉及到一些类似 memory barrier MESI 协议之类的 说实话真的太复杂了,对大部分应用层程序员 你只要了解涉及到多线程数据同步的问题,加锁搞定一切,不加锁一定出事。
    sagaxu
        20
    sagaxu  
       2020 年 3 月 7 日   1
    @liliumss 不是替代 IO 输出,是改变 IO 输出的时机,观测变量时不要有 IO,也不能调用其他可能有副作用的方法,你把观测结果记录下来,等观测完了再调用 IO 输出。System.out.println 是最常见的多线程测试代码的坑,它不仅速度巨慢还自带加锁。构造不正确同步的多线程测试代码,有很多注意点,并且不是所有处理器架构都能重现,有些 sparc 下才有的问题,在 X86 下面却没有。并发的诡异之处在于,你照做了一定对,不照做未必一定错,有些错要构造重现也没那么容易。

    Java(JLS) --> JVM --> 单 CPU --> 多 CPU

    除了内存可见性,还有指令重排,CPU 还有乱序执行,每一个层面都有自己的同步方式,同一层的不同实现还不一样,从 Java 用户的角度,JLS 是法则,是我们唯一能依赖的东西。

    JMM 是给写 JVM 的人看的,不是给 JVM 用户看的,包括 JLS 都不推荐一般人看。

    https://item.jd.com/25578376712.html

    这是 JDK 核心开发者们写的<<Java 并发编程实践>>的中文译本,非常值得反复看。
    liliumss
        21
    liliumss  
    OP
       2020 年 3 月 7 日 via iPhone
    @sagaxu 谢谢 学习了
    az467
        22
    az467  
       2020 年 3 月 8 日
    @liliumss 一般的就是 JSR133 和其他的官方文档,书籍的话就是《 Java 并发编程实践》这种。
    如果要深入分析,那就需要科班的基础知识(编译、汇编、操作系统、硬件等)和一些直觉与经验,还要对 JVM 有一定的了解,这就不是光看文档能学会的了。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     2806 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 45ms UTC 15:08 PVG 23:08 LAX 08:08 JFK 11:08
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86