2010-03-22 58 views
10

我知道,Java中volatile变量的用途是写入这些变量对其他线程立即可见。我也意识到一个同步块的效果之一就是将线程本地内存刷新到全局内存。刷新线程局部内存到全局内存是什么意思?

我从来没有完全理解在这种情况下对'线程本地'内存的引用。我明白,只存在于堆栈上的数据是线程本地的,但是当谈论堆中的对象时,我的理解变得朦胧。

我希望得到以下几点意见:

  1. 当一台机器上有多个处理器上执行,并刷新线程本地存储仅仅指的是CPU缓存到内存的冲洗?

  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物理访问或者如果由VM进行逻辑线程本地堆分配?

任何指向演示或文档的链接都将非常有帮助。我花了很多时间研究这个问题,虽然我发现了很多很好的文献,但我一直无法满足我对线程本地存储器的不同情况定义的好奇心。

非常感谢。

回答

6

你所说的冲水被称为“记忆障碍”。这意味着CPU可以确保从RAM看到的内容也可以从其他CPU /内核看到。这意味着两件事:

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

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

的第二份工作是在单处理器系统微不足道的(我的意思是,与具有单核单CPU系统),但单处理器系统往往成为时下罕见。

至于线程局部堆,这在理论上可以完成,但它通常不值得付出努力,因为没有任何内容可以通过​​来清除内存的哪些部分。这是线程与共享内存模型的局限性:全部内存应该被共享。在遇到的第一个​​中,JVM应该将其所有“线程本地堆对象”都刷新到主RAM中。

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

+0

感谢您的意见。我其实很熟悉逃生分析,它开始与'线程本地'混淆。我想请问两个后续问题: 1.如果编译器已经证明一个对象是线程本地的,并且该对象存在于堆的线程局部区域中,那么为什么要写入该对象内部一个同步块需要刷新?从CPU缓存到线程本地堆区域的刷新只能被写入的线程观察到?这是线程切换处理器,并开始执行一个不同的CPU缓存? – 2010-03-22 21:32:47

+0

2. JVM有可能在堆上的两个单独的内存位置同时存在一个对象吗?如果是这样,在什么情况下会出现这种情况? – 2010-03-22 21:33:22

+0

1.“同步”意味着“全部”的冲洗。 'synchronized'有一个参数,这个参数是获取锁的实例,但是Java内存模型要求来自线程的整个内存视图受到内存障碍的限制。现在,如果JVM可以证明它不需要刷新对象,因为没有其他线程可以看到它(并且“未转义对象”是很好的候选对象),那么JVM就可以在“as if”规则(JVM只要结果与Java抽象机器无法区分就可以完成它所希望的任何事情)。 – 2010-03-23 12:16:14

1

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

当然,有限制,因为所有的内存不是重复的,并且并不是所有的指令都被重新排序,但重要的是如果底层JVM发现它是一种更优化的方式那。

问题是,堆是真的“正确”存储在主内存中,但访问主内存比访问CPU的缓存或将值保存在CPU内的寄存器中要慢。通过要求将值写入内存(这是同步的作用,至少在释放锁时),它强制写入主内存。如果JVM可以自由地忽略它,它可以获得性能。

就一个CPU系统会发生什么而言,即使在执行另一个线程时,多个线程仍然可以将值保存在缓存或寄存器中。不能保证有任何情况下,其他线程无需同步就可以看到一个值,尽管它显然更有可能。当然,在移动设备之外,单CPU正在走软盘之路,所以这不会是一个长期相关的考虑因素。

欲了解更多信息,我推荐Java Concurrency in Practice。这真的是一本关于这个主题的实用书。

+0

这是在旧的Java内存模型(JMM)中指定的,但这已经过去了。 – 2010-03-22 20:58:26

+0

感谢您的评论。这是否意味着同步只是将CPU缓存刷新到主内存?或者,有没有相同的变量可以存在于堆上的两个不同位置的情况? – 2010-03-22 21:10:25

+0

@Jack,不,它也可以指向指令重新排序(所以事情可以写入主内存,但以一种看起来错误的方式),当然还有锁定。我无法想象一个JVM实现在非同步代码中实际上在共享内存中创建一个对象的副本,但我不知道规范中的任何内容都不允许它。 – Yishai 2010-03-22 22:31:37

1

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

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