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

Java 多线程并发,线程什么时候会刷新 "工作内存"

  •  
  •   laoluo1991 · 2019-03-01 12:23:19 +08:00 · 6979 次点击
    这是一个创建于 2126 天前的主题,其中的信息可能已经有所发展或是发生改变。

    讨论一个 Java 多线程相关的问题

    存在线程 A、B, 共享变量 v , B 会循环读取变量 v

    线程 A 对共享变量 v 进行了修改

    线程 B 什么情况下能读到线程 A 修改后的值

    测试代码如下

    目前已知测试
    循环体内为空的时候不停
    循环体内为 Object obj = new Object(); 的时候不停
    循环体内为 System.out.println(); 的时候停
    循环体内为 Thread.sleep(1000); 的时候停
    循环体内为 File file = new File("C:\work\test.txt"); 的时候停
    public class TestDemo {
    
        private  static boolean  keepRunning = true;
    
        public static void main(String[] args)  throws Exception {
            new Thread(
                ()->{
                    while (keepRunning){
                        //do something
                    }
                    System.out.println("循环停止");
                }
            ).start();
            Thread.sleep(1000);
            keepRunning = false;
            System.out.println("下达循环停止指令");
        }
    
    }
    
    
    37 条回复    2020-10-16 14:56:55 +08:00
    vansl
        1
    vansl  
       2019-03-01 12:33:51 +08:00
    是想验证进行 I/O 等导致线程阻塞的操作时会刷新本地内存?貌似没有意义吧。
    momocraft
        2
    momocraft  
       2019-03-01 13:05:47 +08:00   ❤️ 2
    做了( Jawa 内存模型中能保证内存可见性的事,如 volatile / monitor / explicit lock )就应该看得到

    不做未必看不到,但应把看到视作偶然,不要基于巧合编程
    gamexg
        3
    gamexg  
       2019-03-01 14:02:06 +08:00   ❤️ 2
    赞同楼上,不要依赖巧合。
    非 java 程序员,不清楚语言有哪些线程同步机制。
    但是大部分语言都是一样的,如果代码不明确的线程同步,那么编译器、cpu 有可能做出各种缓存、乱序执行,结果会不可预期。
    xomix
        4
    xomix  
       2019-03-01 15:16:48 +08:00
    虽然主语言不是 java,但是赞同不要基于巧合编程的答案。
    peyppicp
        5
    peyppicp  
       2019-03-01 15:30:15 +08:00
    keepRunning 这个变量是在内存上的,并不在 cpu 缓存上,读取运算的时候要先从内存加载到缓存上,如果 cpu 的缓存没有更新,那么读到的就是旧值。
    IO 线程在遇到阻塞时,cpu 会将其切换,在其重新执行时,会重新从内存中加载数据,如 keepRunning,这个时候 keepRunning 已经被更新了,所以循环就停止了。
    codehz
        6
    codehz  
       2019-03-01 15:40:51 +08:00 via Android
    推荐去了解一下 java 的内存模型,这个关键词应该能搜索到相关内容了
    gaius
        7
    gaius  
       2019-03-01 15:42:16 +08:00
    用 volitale
    xzg
        8
    xzg  
       2019-03-01 15:53:59 +08:00
    @peyppicp 赞成楼上的说法,读取 cpu 缓存的值是关键
    neoblackcap
        9
    neoblackcap  
       2019-03-01 16:40:13 +08:00
    @peyppicp JIT 之后 keepRunning 会不会优化成在 CPU 缓存上啊?
    reus
        10
    reus  
       2019-03-01 16:46:04 +08:00
    最怕拿一次两次的测试当真理
    如果内存模型没有保证,那可能下个版本就不是这样了,你这就是埋坑
    peyppicp
        11
    peyppicp  
       2019-03-01 16:52:47 +08:00
    @neoblackcap 在这个 case 下,keepRunning 应该会被 JIT 优化到 main 函数里面的一个局部变量,这个这个玩意是分配在栈上的,栈在内存上,所以还是会在内存上,并不会优化到 cpu 缓存。

    个人想法,欢迎各位探讨
    yidinghe
        12
    yidinghe  
       2019-03-01 16:53:23 +08:00
    多线程访问同一个对象或变量,要严格进行同步操作。最简单的办法就是 synchronized 关键字。
    turnrut
        13
    turnrut  
       2019-03-01 16:55:49 +08:00
    跟 java 内存模型没太大关系, cpu 为了性能会优先从自己的独立高速缓存(程序无法感知)操作数据, intel 的指令里专门提供了一个前缀 F0H 强制使用主内存.
    The LOCK prefix (F0H) forces an operation that ensures exclusive use of shared memory in a multiprocessor environment.
    详见 Intel® 64 and IA-32 Architectures Software Developer's Manuals Vol. 2A 2.2.1
    链接 https://software.intel.com/sites/default/files/managed/39/c5/325462-sdm-vol-1-2abcd-3abcd.pdf
    neoblackcap
        14
    neoblackcap  
       2019-03-01 17:09:23 +08:00
    @peyppicp
    @turnrut

    感觉还是 @turnrut 说得对啊。不过这样又会牵涉到硬件,毕竟这代码只是 Java。比如这个 JVM 是跑在非 x86 的 CPU 上,那结果大概也会不一样。
    turnrut
        15
    turnrut  
       2019-03-01 17:15:08 +08:00
    上面说的有点问题, 专门有几个指令用来刷新 cpu cache 的
    比如 CLFLUSH — Flush Cache Line
    https://www.felixcloutier.com/x86/clflush
    peyppicp
        16
    peyppicp  
       2019-03-01 17:15:38 +08:00   ❤️ 1
    @neoblackcap 是我说的不清楚? cpu 执行指令的时候要先从内存加载到缓存,我已经说明过了。JIT 优化 keepRunning 之后,keepRunning 也会分配到内存上,执行的时候加载到 cpu 缓存里。JIT 是不能直接优化到 cpu 缓存里面的
    neoblackcap
        17
    neoblackcap  
       2019-03-01 17:29:48 +08:00
    @peyppicp 是啊,会加载到缓存。那么程序应该是直接读缓存的吧?那么比如线程 1 在核心 1 上跑,线程 2 在核心 2 上跑,线程 1 将 keepRunning 设成 false,这里没有同步的话,这个 keepRunning 的值按道理不会立刻刷新核心 2 的高速缓存吧。什么时候线程 2 停止应该是不确定的。
    gtexpanse
        18
    gtexpanse  
       2019-03-01 17:46:17 +08:00
    你得到的结论只是巧合——如果严格点从 jvm 的角度来说(其实这个“什么时候刷新”跟 jvm 也没啥关系)。
    gamexg
        19
    gamexg  
       2019-03-01 18:03:52 +08:00
    @neoblackcap cpu 高速缓存问题倒是不用担心,cpu 硬件可以保证 cpu 核 1 更新了内存时其他核心的缓存会失效。
    gamexg
        20
    gamexg  
       2019-03-01 18:06:04 +08:00
    @gamexg #19 但是这里只是直接读写内存硬件上可以保证高速缓存不会是旧数据。

    应用程序自己从内存读到寄存器的数据不会受这个保护,还会是旧值。
    letianqiu
        21
    letianqiu  
       2019-03-01 18:06:11 +08:00
    @peyppicp 你是基于发生 context switch 的时候 CPU 会 flush 掉 cache,这个不一定成立。
    zjp
        22
    zjp  
       2019-03-01 18:11:46 +08:00 via iPhone
    System.out.println();方法有 synchronized 修饰,使得虚拟机很有可能刷新本地内存。然后有些错误的并发代码加了行输出做调试就看起来正常了……
    neoblackcap
        23
    neoblackcap  
       2019-03-01 18:16:58 +08:00 via iPhone
    @gamexg 我记得哪怕 x86 也要加对应的内存屏障啊
    gamexg
        24
    gamexg  
       2019-03-01 18:25:06 +08:00   ❤️ 1
    @neoblackcap #23 关键字 高速缓存一致性
    Banxiaozhuan
        25
    Banxiaozhuan  
       2019-03-01 18:41:44 +08:00
    @neoblackcap 我咋感觉这些回复都很水。。。。
    都撤到了系统架构。。。 傻不傻,, 看看七楼回答的这个 volitale。
    人家都帮你做好了,还在乱研究,多读书,别做无头苍蝇。
    fuyufjh
        26
    fuyufjh  
       2019-03-01 19:07:58 +08:00
    memory barrier

    ps. JVM 内存模型就像 java 标准一样,是给 JVM 开发者看的。各位 Java 用户直接去搞懂 CPU cache 就足够了
    choice4
        27
    choice4  
       2019-03-01 19:11:56 +08:00 via Android
    为了提升性能,线程里面有工作内存,这样访问数据不用去主存读取,可以快一些。共享变量被线程修改后,该线程的工作内存中的值就会和其他线程不一致,也和主存的值不一致,所以需要将工作内存的值刷入主存,但是这个刷入可能其他线程并没有看到。使用 volatile 后可以通过 cpu 指令屏障强制要求读操作发生在写操作之后,并且其他线程在读取该共享变量时,需要先清理自己的工作内存的该值,转而重新从主存读取,volatile 保证一定会刷新,但是不写也不一定其他线程看不见。
    就是上面大哥说的巧合(即这种不一定每次都会有正确的保障)
    HhZzXx
        28
    HhZzXx  
       2019-03-01 19:22:14 +08:00
    推荐看 java concurrent in practice
    asd123456cxz
        29
    asd123456cxz  
       2019-03-01 19:58:48 +08:00
    同意解决问题的方式使用 volatile,这是字节码指令->内存屏障的事。至于(无同步操作下,何时工作内存数据刷到主存)这个问题我也想过,如果有大神了解希望解答。
    cyspy
        30
    cyspy  
       2019-03-01 20:02:21 +08:00
    如果不加 volatile,应该是写入方的 cache 被刷新到主存之后,读取方 cache 失效的时候,从主存里取到新值
    turnrut
        31
    turnrut  
       2019-03-01 21:13:44 +08:00 via Android
    @asd123456cxz 抛开硬件中断的情况,cpu 顺序执行内存里的指令,假设它的高速缓存是 1k,当它开始执行 3k 位置处的指令,写回原缓存,并把 3-4k 的数据度入缓存里,在执行出这个范围外前一定会写回内存。至于在这个缓存范围内循环执行,不保证是否写回和写回的频率。
    再来谈中断的情况,中断后会去执行预设固定位置的代码,简单的把它看成一次大跳转,中断前后一定会刷新缓存。然后系统内核提供给用户空间的接口都是(软)中断实现的,比如读取一个文件。即使不用内核的中断写一个死循环,但是还有最基础的硬件时间中断,比如进程和线程的调度就靠它。
    这个问题分成两层,如果想写正确的 java 代码,那只需要清楚 java 里几个关键字的语义。原理的话,天然离不开 cpu 和操作系统这些底层的东西,每一层抽象都为下一层提供语义上的保证,代码最终还是老老实实的跑在硬件上。
    yuyujulin
        32
    yuyujulin  
       2019-03-01 22:22:27 +08:00
    @choice4 这个主内存是不是就是 CPU 的缓存呢?
    choice4
        33
    choice4  
       2019-03-01 23:23:43 +08:00 via Android
    @yuyujulin java 里边可以简单理解为 jvm 堆区,
    asd123456cxz
        34
    asd123456cxz  
       2019-03-02 20:26:14 +08:00
    @turnrut #31 感谢大佬。话说出于好奇看了下你之前的回复。。感觉好强啊,同样是自学 Java 差距巨大,不介意的话可以加个微信交流下吗?我的微信是 sul609。或者讲讲大佬你的学习路线学习途经什么的也是极好的!
    yuyujulin
        35
    yuyujulin  
       2019-03-02 21:04:25 +08:00
    @choice4 那这么说的话,跟 CPU 缓存是没什么关系咯?
    choice4
        36
    choice4  
       2019-03-02 21:39:09 +08:00 via Android
    @yuyujulin 主存主要包括本地方法区和堆区,线程工作内存主要包括 线程私有的栈区和对主存中部分变量拷贝的寄存器(包括程序计数器和 cpu 高速缓存)
    lswang
        37
    lswang  
       2020-10-16 14:56:55 +08:00
    楼主应该是不明白 while 循环里面是空的时候,为何循环停不了吧。(因为一开始我也想不明白为什么)
    停不了的原因是 JIT 作祟了。楼主可以在 java 运行参数中加上 -Xint,while 里面即使为空,也是可以结束的
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1089 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 19:01 · PVG 03:01 · LAX 11:01 · JFK 14:01
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.