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

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

  •  1
     
  •   liliumss · 2020-03-06 22:15:08 +08:00 · 3045 次点击
    这是一个创建于 1730 天前的主题,其中的信息可能已经有所发展或是发生改变。

    小弟正在学 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-03-06 22:38:18 +08:00
    你定义了一个静态变量。。
    kevincai100
        2
    kevincai100  
       2020-03-06 22:41:31 +08:00   ❤️ 1
    静态变量在方法区,共享内存,被别人改了立即可见的, 应该不是线程里面的副本
    fihserman123
        3
    fihserman123  
       2020-03-06 22:51:48 +08:00
    静态变量在方法区( JDK8 后是 meta space )中,线程或者说 run 方法的变量存于栈中。而位于 enclosing class 中的静态变量 private static Long count = 0L; 对于 run 方法而言是可见的,换句话说,你代码里的三个 count 存有同一个 int 数值,且相互影响。你需要做的是在俩线程的 run 方法中分别额外定义一个 count 变量。
    fihserman123
        4
    fihserman123  
       2020-03-06 22:52:23 +08:00
    静态变量貌似去堆中了(逃。
    sagaxu
        5
    sagaxu  
       2020-03-06 22:57:02 +08:00 via Android
    jmm 哪个条款说互不影响了?
    liliumss
        6
    liliumss  
    OP
       2020-03-06 22:58:21 +08:00
    @fihserman123 但是我顶一个静态的 boolean 值确是 2 线程互不影响的 这是什么原理呢
    liliumss
        7
    liliumss  
    OP
       2020-03-06 23:01:51 +08:00
    @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-03-06 23:03:47 +08:00
    @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-03-06 23:31:30 +08:00   ❤️ 1
    @liliumss 没有定义 volatile 导致 thread 3 寄存器的里的值一直是旧的. 由于非 volatile 的, 这里 thread 3 跑的那个 cpu 可以无限期的去使用寄存器缓存里面存放的 count 值.(当然这个行为是不定的, 不同机器上表现也不会一致)

    而题目里那种场景, 因为 thread 1 有更新, 等于是和主内存有交互(其实是 L1 cache), 寄存器的值就被更新成最新的了. 一般硬件的 MESI 协议会保证各个 cpu 核上看见的值是一致(大体是这种意思, 更具体的可以搜搜 MESI)
    liliumss
        10
    liliumss  
    OP
       2020-03-06 23:49:07 +08:00
    @Jooooooooo 谢谢你的回答
    意思就是线程 1 的那个 count++ 触发了 MESI 协议与主内存有交互了,而正好线程 2 把 count 值改变了所以线程 1 就直接满足条件推出了
    而线程 3 一直没更新,又没使用 volatile 保证可见性,所以即使线程 4 更改了 boolean 值也无法从循环跳出
    我理解的对吧,关键就是线程 1 的 count++导致了 2 种不同变量在后面操作的差别
    az467
        11
    az467  
       2020-03-07 00:08:30 +08:00
    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-03-07 00:32:56 +08:00 via iPhone
    @az467 我本地也是这个结果 我纳闷是根据 jmm 搜第一个 thread 还在在循环 assgin count 的时候 第二个线程结束同步 write 给主存的值为啥影响了第一个线程的 count 这里并没设置 valiate 修饰 而用 boolan 做的 demo (见楼层)确是可以的
    sagaxu
        13
    sagaxu  
       2020-03-07 00:50:57 +08:00   ❤️ 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-03-07 02:39:38 +08:00   ❤️ 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-03-07 08:11:19 +08:00 via iPhone
    @sagaxu 谢谢 请问使用什么方法替代 system.out.print 呢
    liliumss
        16
    liliumss  
    OP
       2020-03-07 08:12:45 +08:00 via iPhone
    @az467 谢谢 请问看这方面知识有啥好文档呢
    yanyueio
        17
    yanyueio  
       2020-03-07 08:56:22 +08:00 via Android
    找本靠谱点的 JAVA 语法关键字指北吧,然后再去理解 jvm,jmm,并且一定注意 jdk/jre 实现版本。同楼上,很多优化导致了不确定,具体根据想象以及自己的操作系统功底,具体分析。
    lewis89
        18
    lewis89  
       2020-03-07 10:49:00 +08:00   ❤️ 1
    多线程研究这些变量之间的一致性,说实话真的没必要,加锁就好了 锁的语义就有 invalid cache 从主内存读 然后 invalid cache 写入主内存的意思
    lewis89
        19
    lewis89  
       2020-03-07 10:53:54 +08:00
    中间还涉及到一些类似 memory barrier MESI 协议之类的 说实话真的太复杂了,对大部分应用层程序员 你只要了解涉及到多线程数据同步的问题,加锁搞定一切,不加锁一定出事。
    sagaxu
        20
    sagaxu  
       2020-03-07 11:55:20 +08:00   ❤️ 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-03-07 19:36:16 +08:00 via iPhone
    @sagaxu 谢谢 学习了
    az467
        22
    az467  
       2020-03-08 13:38:20 +08:00
    @liliumss 一般的就是 JSR133 和其他的官方文档,书籍的话就是《 Java 并发编程实践》这种。
    如果要深入分析,那就需要科班的基础知识(编译、汇编、操作系统、硬件等)和一些直觉与经验,还要对 JVM 有一定的了解,这就不是光看文档能学会的了。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2823 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 39ms · UTC 12:02 · PVG 20:02 · LAX 04:02 · JFK 07:02
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.