Java 多线程并发,线程什么时候会刷新 "工作内存" - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
laoluo1991
V2EX    Java

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

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

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

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

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

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

    测试代码如下

    目前已知测试
    循环体内为空的时候不停
    循环体内为 Obect 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 停止应该是不确定的。
    syncnano
        18
    syncnano  
       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     Solana     2941 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 42ms UTC 13:39 PVG 21:39 LAX 06:39 JFK 09:39
    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