刷新线程本地内存到全局内存是指什么?

10

我知道在Java中使用volatile变量的目的是为了使对这些变量的写操作可以立即被其他线程看到。我也知道同步块的一个效果是将线程本地内存刷新到全局内存。

但我从来没有完全理解在这种情况下 'thread-local' 内存的引用。我知道仅存在于堆栈上的数据是线程本地的,但当涉及到堆上的对象时,我的理解就变得模糊了。

我希望能够得到以下几点的评论:

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

  2. 在单处理器机器上执行时,这是否有任何意义?

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

  4. (编辑:添加问题4) 退出同步块时会刷新哪些数据?它是线程本地所有内容吗?还是仅限于在同步块内进行的写操作?

  5. 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可以物理访问该内存,或者虚拟机是否进行逻辑线程本地堆分区?

    任何演示文稿或文档的链接都将非常有帮助。我已经花费了时间研究这个问题,虽然我找到了很多好文献,但我还没有能够满足我的好奇心,了解线程本地内存的不同情况和定义。

    非常感谢。

4个回答

6
你所说的"flush"被称为"内存屏障"。这意味着CPU确保它从RAM中看到的内容也可以被其他CPU/核心看到。它暗示了两件事:
- JIT编译器刷新CPU寄存器。通常,代码可能会在CPU寄存器中保存一些全局可见数据(例如实例字段内容)。寄存器无法被其他线程看到。因此,“synchronized”的一半工作是确保不维护此类缓存。 - "synchronized"实现还执行内存屏障,以确保来自当前核心的所有RAM更改被传播到主RAM(或者至少所有其他核心都知道该核心具有最新值 - 缓存一致性协议可能非常复杂)。
第二个任务在单处理器系统上很容易(我的意思是,具有单核心的单个CPU系统),但单处理器系统现在越来越少。
至于线程本地堆,理论上可以做到,但通常不值得付出努力,因为没有任何东西告诉我们哪些内存部分需要使用"synchronized"刷新。这是线程共享内存模型的限制:所有内存都应该是共享的。在遇到第一个"synchronized"时,JVM应该将其所有的“线程本地堆对象”刷新到主RAM中。
然而,Sun最近的JVM可以执行“逃逸分析”,其中JVM成功地证明某些实例永远不会从其他线程中可见。例如,这是由javac创建的用于处理字符串连接的StringBuilder实例。如果该实例从未作为参数传递给其他方法,则不会变为“全局可见”。这使其有资格进行线程本地堆分配,甚至在正确情况下,进行基于堆栈的分配。请注意,在这种情况下没有重复;该实例不在“同时两个地方”。只是JVM可以将实例保存在私有位置中,而不会产生内存屏障的成本。

谢谢您的评论。我对逃逸分析很熟悉,这也导致了我对“线程本地”产生了困惑。我想请问两个后续问题:
  1. 如果编译器已经证明一个对象是线程本地的,并且该对象存在于堆的线程本地区域中,那么为什么在同步块内写入此对象需要刷新?从CPU缓存到线程本地堆区域的刷新只会被进行写操作的线程观察到?这是为了防止线程切换处理器并开始使用不同的CPU缓存吗?
- ares
  1. JVM是否可能使一个对象同时存在于堆上的两个不同的内存位置?如果是,那么在什么情况下会出现这种情况?
- ares
synchronized 表示“一切都要刷新”。synchronized 有一个参数,即获取锁的实例,但是 Java 内存模型规定线程的整个内存视图都受到内存屏障的影响。现在,如果 JVM 可以证明它不需要刷新对象,因为没有其他线程可能会看到它(而“未逃逸的对象”是很好的候选对象),那么 JVM 可以自由地不刷新,根据“好像”规则(只要结果与 Java 抽象机器无法区分,JVM 可以做任何它想做的事情)。 - Thomas Pornin
在Java框架内,一个实例就是一个实例,而不是两个,所有对该实例的引用必须相等。如果JVM在底层复制一个对象,则必须小心地进行引用比较,使得这些副本看起来像是单个实例。一些GC会在RAM中“移动”对象,这意味着某个对象存在于“两个位置”的时候。Sun的JVM中默认的GC会使这种情况短暂出现;副本只会在“暂停”期间出现,此时所有线程都停止。其他一些GC类型可能会容忍长期存在的副本。 - Thomas Pornin

1

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

当然,有限制,因为并非所有内存都保留了副本,也不是所有指令都被重新排序,但重点是底层JVM有选择权,如果发现更优化的方式,则可以使用它。

问题在于堆实际上是“正确地”存储在主内存中的,但访问主内存与访问CPU缓存或将值保存在CPU内部寄存器中相比较慢。通过要求将值写入内存(这就是同步所做的,至少当释放锁时),它强制进行写入主内存。如果JVM可以忽略它,就可以提高性能。

关于单CPU系统会发生什么事情,多个线程仍然可以保持缓存或寄存器中的值,即使在执行另一个线程时。没有保证有任何情况下值对另一个线程可见而无需同步,尽管这显然更有可能发生。当然,在移动设备之外,单CPU正走向软盘的路子,所以这不会是一个非常重要的考虑因素。

如需更多阅读,我推荐Java并发编程实践。这是一本非常实用的关于该主题的好书。


它曾经在旧版Java内存模型(JMM)中被指定,但那已经过去了。 - Tom Hawtin - tackline
感谢您的评论。这是否意味着同步只是指将CPU缓存刷新到主内存?或者,是否存在同一变量在堆上的两个不同位置的情况? - ares
@Jack,不,它也可以指指令重排(因此可以将东西写入主内存,但以看似错误的方式),当然还有锁定。我无法想象JVM实现会在非同步代码中复制共享内存中的对象,但我不知道规范中是否允许这样做。 - Yishai

1

这并不像CPU缓存RAM那样简单。这些都包含在JVM和JIT中,它们添加了自己的行为。

看一下“双重检查锁定已经失效”的声明。这是一篇关于为什么双重检查锁定不起作用的论文,但也解释了Java内存模型的一些微妙之处。


1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接