【问题标题】:What does flushing thread local memory to global memory mean?将线程本地内存刷新到全局内存是什么意思?
【发布时间】:2011-01-30 12:57:23
【问题描述】:

我知道 Java 中 volatile 变量的目的是写入此类变量对其他线程立即可见。我也知道同步块的作用之一是将线程本地内存刷新到全局内存。

我从未完全理解在这种情况下对“线程本地”内存的引用。我知道仅存在于堆栈中的数据是线程本地的,但是当谈论堆上的对象时,我的理解变得模糊。

我希望能够让 cmets 了解以下几点:

  1. 在多处理器机器上执行时,刷新线程本地内存是否只是将 CPU 缓存刷新到 RAM 中?

  2. 在单处理器机器上执行时,这意味着什么吗?

  3. 如果堆有可能在两个不同的内存位置(每个由不同的线程访问)具有相同的变量,在什么情况下会出现这种情况?这对垃圾收集有什么影响?虚拟机做这种事情有多积极?

  4. (编辑:添加问题 4) 退出同步块时会刷新哪些数据?它是线程在本地拥有的一切吗?是否仅在同步块内进行写入?

    Object x = goGetXFromHeap(); // x.f is 1 here    
    Object y = goGetYFromHeap(); // y.f is 11 here
    Object z = goGetZFromHead(); // z.f is 111 here
    
    y.f = 12;
    
    synchronized(x)
    {
        x.f = 2;
        z.f = 112;
    }
    
    // will only x be flushed on exit of the block? 
    // will the update to y get flushed?
    // will the update to z get flushed?
    

总的来说,我想了解线程本地是指仅可由一个 CPU 物理访问的内存,还是虚拟机完成逻辑线程本地堆分区?

任何指向演示文稿或文档的链接都会非常有帮助。我花了很多时间研究这个,虽然我找到了很多不错的文献,但我无法满足我对线程本地内存的不同情况和定义的好奇心。

非常感谢。

【问题讨论】:

    标签: java memory model multithreading


    【解决方案1】:

    您所说的刷新被称为“内存屏障”。这意味着 CPU 确保它所看到的 RAM 也可以从其他 CPU/内核中查看。这意味着两件事:

    • JIT 编译器刷新 CPU 寄存器。通常,代码可能会在 CPU 寄存器中保留一些全局可见数据(例如实例字段内容)的副本。从其他线程看不到寄存器。因此,synchronized 的一半工作是确保不维护此类缓存。

    • synchronized 实现还执行内存屏障,以确保从当前内核对 RAM 的所有更改都传播到主 RAM(或者至少所有其他内核都知道该内核具有最新的values -- 缓存一致性协议可能相当复杂)。

    第二个工作在单处理器系统上是微不足道的(我的意思是,具有作为单核的单个 CPU 的系统),但如今单处理器系统往往变得越来越少。

    至于线程局部堆,理论上是可以做到的,但通常不值得付出努力,因为没有什么可以告诉synchronized要刷新内存的哪些部分。这是线程共享内存模型的一个限制:所有内存应该是共享的。在第一次遇到synchronized 时,JVM 应将其所有“线程本地堆对象”刷新到主 RAM。

    然而最近来自 Sun 的 JVM 可以执行“逃逸分析”,其中 JVM 成功地证明某些实例永远不会从其他线程可见。这是典型的,例如,由javac 创建的StringBuilder 实例来处理字符串的连接。如果实例从未作为参数传递给其他方法,则它不会变为“全局可见”。这使它有资格进行线程局部堆分配,甚至在适当的情况下,也可以进行基于堆栈的分配。请注意,在这种情况下没有重复;该实例不是“同时在两个地方”。只是 JVM 可以将实例保存在一个私有的地方,这不会产生内存屏障的成本。

    【讨论】:

    • 感谢 cmets。我实际上对逃逸分析很熟悉,它让我对“线程本地”感到困惑。我想问两个后续问题:1.如果编译器已经证明一个对象是线程本地的,并且该对象存在于堆的线程本地区域中,那么为什么要在同步的内部写入这个对象块需要被刷新?从 CPU 缓存到线程本地堆区域的刷新只能由进行写入的线程观察到吗?这是因为线程切换处理器并开始使用不同的 CPU 缓存执行吗?
    • 2. JVM 是否有可能让一个对象同时存在于堆上的两个单独的内存位置?如果有,在什么情况下会出现这种情况?
    • 1. synchronized 意味着“一切”的冲洗。 synchronized 有一个参数,它是获取锁的实例,但 Java 内存模型要求线程的整个内存视图受内存屏障的影响。现在,如果 JVM 可以证明它不需要刷新对象,因为没有其他线程可以看到它(并且“未转义的对象”是很好的候选对象),那么 JVM 可以根据“好像”规则(JVM只要结果不能与 Java 抽象机区分开来,它就可以做任何事情)。
    • 2.在 Java 框架中,一个实例是一个实例,而不是两个实例,并且对该实例的所有引用都必须比较相等。如果,在后台,JVM 复制了一个对象,那么它必须小心地进行引用比较,以使复制看起来好像它们是单个实例一样。一些 GC 在 RAM 中“移动”对象,这意味着有时给定对象存在于“两个地方”。 Sun 的 JVM 中的默认 GC 使这种情况变得短暂;重复仅在“暂停”期间发生,所有线程都停止。其他一些 GC 类型可能会容忍长期重复。
    【解决方案2】:

    如果一个未同步对象的当前内存内容对另一个线程可见,这实际上是一个实现细节。

    当然,这是有限制的,因为所有内存都不会重复保存,也不是所有指令都被重新排序,但关键是底层 JVM 可以选择,如果它发现它是一种更优化的方式来做到这一点.

    问题是堆确实“正确”地存储在主存中,但是与访问 CPU 的缓存或将值保存在 CPU 内部的寄存器中相比,访问主存的速度很慢。通过要求将值写入内存(这是同步所做的,至少在释放锁时),它强制写入主内存。如果 JVM 可以随意忽略这一点,它可以获得性能。

    就在一个 CPU 系统上发生的情况而言,多个线程仍然可以将值保存在缓存或寄存器中,即使在执行另一个线程时也是如此。不能保证在没有同步的情况下存在任何值对另一个线程可见的情况,尽管它显然更有可能。当然,在移动设备之外,单 CPU 正在走软盘的道路,所以这不会是一个非常相关的考虑因素。

    更多阅读,我推荐Java Concurrency in Practice。这确实是一本关于该主题的实用书籍。

    【讨论】:

    • 它是在旧的 Java 内存模型 (JMM) 中指定的,但早已不复存在。
    • 感谢 cmets。这是否意味着同步只是指将 CPU 缓存刷新到主内存?或者,是否存在同一个变量可以存在于堆上的两个不同位置的情况?
    • @Jack,不,它也可以指指令重新排序(因此可以将内容写入主存储器,但以一种看起来错误的方式),当然还有锁定。我无法想象一个 JVM 实现实际上会在非同步代码中在共享内存中复制一个对象,但我不知道规范中有什么不允许这样做。
    【解决方案3】:

    它不像 CPU-Cache-RAM 那样简单。这一切都包含在 JVM 和 JIT 中,它们添加了自己的行为。

    看看The "Double-Checked Locking is Broken" Declaration。这是一篇关于为什么双重检查锁定不起作用的论文,但它也解释了 Java 内存模型的一些细微差别。

    【讨论】:

      【解决方案4】:

      JavaOne 2009 Technical Session 中的 PDF 是一个突出所涉及问题种类的优秀文档

      This Is Not Your Father's Von Neumann Machine: How Modern Architecture Impacts Your Java Apps

      作者:Cliff Click,Azul Systems; Brian Goetz,Sun Microsystems, Inc.

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-10-22
        • 1970-01-01
        • 1970-01-01
        • 2012-04-03
        • 1970-01-01
        • 2013-05-21
        • 2015-04-01
        • 1970-01-01
        相关资源
        最近更新 更多