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

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

  •  
  •   laoluo1991 · 332 天前 · 3787 次点击
    这是一个创建于 332 天前的主题,其中的信息可能已经有所发展或是发生改变。

    讨论一个 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("下达循环停止指令");
        }
    
    }
    
    
    36 回复  |  直到 2019-03-02 21:39:09 +08:00
    vansl
        1
    vansl   332 天前
    是想验证进行 I/O 等导致线程阻塞的操作时会刷新本地内存?貌似没有意义吧。
    momocraft
        2
    momocraft   332 天前   ♥ 1
    做了( Jawa 内存模型中能保证内存可见性的事,如 volatile / monitor / explicit lock )就应该看得到

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

    个人想法,欢迎各位探讨
    yidinghe
        12
    yidinghe   332 天前
    多线程访问同一个对象或变量,要严格进行同步操作。最简单的办法就是 synchronized 关键字。
    turnrut
        13
    turnrut   332 天前
    跟 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   332 天前
    @peyppicp
    @turnrut

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

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

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