1

在 Java 中,假设您有两个线程 T1 和 T2 同时在两个不同的处理器 P1 和 P2 上运行。

首先,线程 T2 使用obj在(例如)起始内存位置 0x1000 分配的某个对象。这会导致 P2 在该内存位置内部缓存该值。T2 然后将(唯一的)对对象的引用清空,并对其进行垃圾收集。

线程 T1 然后执行

    Foo fooRef = new Foo();
    fooRef.x = 10;

恰好它fooRef.x的位置也在 0x1000,因为 Foo 的这个实例被分配了重新使用上面 T2 释放的内存。

T1 然后将 fooRef 引用传递给线程 T2(通过队列或其他一些共享内存机制)。

T2 会看到以前的旧缓存值,还是会看到新值 10?

假设没有硬件缓存一致性机制。Java 本身是否确保在为对象释放或分配内存时清除每个处理器的缓存?(即使有硬件缓存一致性机制,一致性传播也不是瞬时的,如果 Java 本身没有采取其他一致性措施,T2 可能仍然会读取过时的值)。

4

4 回答 4

5

如果您没有正确同步,则 T2 原则上可能会看到以下三种情况之一(不一定具有相同的概率):

  • (a) 一个看似正确形成的对象,但包含不正确的数据;

  • (b) 一开始就没有正确形成的对象(即,别管你的数据,属于该对象的实际内务元数据不正确可见,可能导致“坏事发生”);

  • (c) 意外地,你“躲过了子弹”,而 T2 在 T1 离开时看到了该物体。

如果您正确同步(或换句话说,正确发布对象),那么 T2 将看到 T1 定义的对象。在这篇关于最终关键字的文章和底部链接的进一步文章中,我讨论了一些问题和解决方案。其中一些回答了上一个关于什么是对象发布以及我们为什么需要它的问题?也可能有帮助。

因此,实际上[*] 一直都需要正确同步。如果您没有正确同步,尝试猜测会发生 (a)、(b) 或 (c) 中的哪种情况是危险的。

[*] 有一些非常偶然的高级技术可以安全地避免同步,如果你能真正计算出由于缺乏同步而导致的所有可能的“路径”,例如一种称为同步捎带的技术,你可以有效地知道同步将是在其他地方“及时”执行。我建议你不要走这条路!

于 2012-12-13T00:23:36.160 回答
1

您不会看到第一个对象留下的“垃圾”。

对象中的每个原语都将包含其初始值(0false等)或在某个时间点放置在那里的某个值——尽管重新排序可能会产生奇怪的值混合。此外,如果原语是两个字的值(longdouble),您可能只会看到其中一个字被更新:这可能会产生一个没有线程曾经放在那里的值,但它与上述一致,因为您看到的是写入该对象的效果——你只是没有看到所有的写入。但是您仍然没有看到写入对一些完全其他的随机对象的影响。

对于参考值,您将看到初始值null(其他线程已放入,允许重新排序等)。

现在,我实际上无法在 JLS 中找到编写此内容的确切位置。但有几个部分强烈暗示了这一点。例如,JLS 17.4.5举例说明:

由于没有同步,每次读取都可以看到初始值的写入或其他线程的写入。

强调我的,但请注意它列出了读取可以看到的值;它并没有说“每次读取都可以看到任何东西,包括以前对象留下的垃圾字节。”

此外,在 17.4.8 中,另一个示例指出:

由于读取在每个线程中首先出现,因此执行顺序中的第一个操作必须是读取。如果该读取无法看到稍后发生的写入,则它无法看到除了它读取的变量的初始值之外的任何值。

(再次强调我的)。请注意,尽管它是在一个示例中而不是在“主体”主体中,但它明确表示不允许按照您的描述进行垃圾读取。

然后,JLS 17.7完全是关于 64 位原语的非原子性(我上面提到的longdouble值)。同样,如果绝对不能保证您看到的字节数,那么注意到您可以从一次写入中看到一个单词而从另一次写入中看到另一个单词是没有意义的。换句话说,JLS 说您可以看到仅由一个正在更新的单词产生的“损坏”值这一事实强烈暗示您不到仅由完整的剩余垃圾产生的“损坏”值.

于 2012-12-13T01:17:42.030 回答
0

Java 无法访问底层硬件缓存,因此它不能“确保清除每个处理器的缓存”。

大多数现代、真实的 CPU 都提供高速缓存一致性。在某些情况下,一些真正的 CPU需要内存屏障。在所描述的条件下,您假设的没有硬件机制的 CPU 可能会遭受过时缓存的影响。

于 2012-12-13T00:08:53.470 回答
0

只要对fooRef和的访问fooRef.x正确同步,线程T2就会看到 的最新值fooRef.x,即 10。

于 2012-12-13T00:18:19.597 回答