在Java中,假设您有两个线程T1和T2在两个不同的处理器P1和P2上同时运行。
首先,线程T2使用在(例如)起始内存位置0x1000处分配的某个对象obj
。这会导致P2在内部缓存该内存位置的值。 T2然后将(仅)对该对象的引用置空并且它被垃圾收集。
线程T1
Foo fooRef = new Foo();
fooRef.x = 10;
并且恰好fooRef.x
的位置也是0x1000,因为这个Foo实例被重新分配了上面用T2释放的内存。
T1将fooRef引用传递给线程T2(通过队列或其他一些共享内存机制)。
T2会看到之前旧的过时缓存值,还是会看到10的新值?
假设没有硬件缓存一致性机制。当Java为对象解除分配或分配内存时,它本身是否确保清除每个处理器的缓存? (即使有硬件缓存一致性机制,一致性传播也不是即时的,如果没有采用Java本身的其他一致性措施,T2可能仍然会读取陈旧值。)
答案 0 :(得分:5)
如果你没有正确同步,那么T2原则上可以看到三件事之一(不一定具有相同的概率):
(a)一个看似正确形成的对象,但包含不正确的数据;
(b)首先没有正确形成的对象(即不介意你的数据,属于该对象的实际管家元数据不能正确显示,可能导致“坏事发生”)
(c)意外地,你“躲闪子弹”,T2看到了物体,因为T1离开了它。
如果你正确地同步(或换句话说,正确发布对象),那么T2将看到对象为T1定义它。在这篇article on the final keyword以及与底部相关的其他文章中,我将讨论一些问题和解决方案。对What is object publishing and why do we need it?上一个问题的一些回答也可能有所帮助。
所以,实际上[*]所有的时间,你需要正确同步。如果您没有正确同步,尝试猜测哪些情况(a),(b)或(c)会发生是危险的。
[*]如果您可以真正计算因缺乏同步而导致的所有可能的“路径”,例如synchronisation piggybacking所谓的{{3}}技术,那么非常偶然的高级技术可以安全地避免同步。有效地知道同步将在其他地方“及时”执行。我建议你不要走这条路!
答案 1 :(得分:1)
你不会在第一个物体上看到“垃圾”。
对象中的每个基元都将包含其初始值(0
,false
等)或某些值已放置在某处 - 尽管重新排序可能会产生奇怪的值混合。另外,如果一个原语是一个双字值(long
或double
),你可能会看到只更新了这些字中的一个:这可能产生一个没有线程放在那里的值,但它是与上面的内容一致,你看到了写这个对象的效果 - 你只是没有看到所有的写。但是你仍然没有看到写入对其他一些随机对象的影响。
对于参考值,您将看到初始值(null
)或对构造对象的正确引用 - 尽管该对象的值遵循与上述相同的模糊规则(它们可以是初始值或其他一些线程放入的任何其他值,允许重新排序等。)
现在,我实际上无法在JLS中找到确切位置。但有几个部分强烈暗示它。例如,JLS 17.4.5在一个例子中说明:
由于没有同步,每次读取都可以看到 写入初始值或写入另一个线程。
强调我的,但请注意它列出了读取可以看到的值;它并没有说“每次读取都可以看到任何东西,包括从以前的对象遗留下来的垃圾字节。”
此外,在17.4.8中,另一个例子说明:
由于读取在每个线程中排在第一位,因此执行顺序中的第一个操作必须是读取操作。如果该读取无法看到稍后发生的写入,那么除了它所读取的变量的初始值之外,它看不到任何值。
(再次强调我的意思)。请注意,虽然它在一个示例中而不是在“main”主体中,但明确表示不允许使用您描述的垃圾读取。
然后,JLS 17.7完全是关于64位基元的非原子性(我上面提到的long
和double
值)。同样,如果绝对不能保证你看到的字节,那么注意你可以看到一个写入的一个字和另一个写入的另一个字是没有意义的。换句话说,JLS表示您可以看到只有一个单词正在更新时出现的“损坏”值这一事实强烈建议您无法看到出现的“损坏”值从刚完成的左手垃圾。
答案 2 :(得分:0)
Java无法访问底层硬件缓存,因此它“无法确保清除每个进程缓存”。
大多数现代的真实CPU都提供缓存一致性。在某些情况下,某些真正的CPU require a memory barrier。没有硬件机制的假设CPU可能会在所描述的条件下遭受过时的缓存。
答案 3 :(得分:0)
只要正确同步fooRef
和fooRef.x
的访问,线程T2
就会看到fooRef.x
的最新值,即10。