尽管没有代码明确泄漏,未初始化的对象泄漏到另一个线程?

让我们看看这个简单的Java程序:

import java.util.*; class A { static B b; static class B { int x; B(int x) { this.x = x; } } public static void main(String[] args) { new Thread() { void f(B q) { int x = qx; if (x != 1) { System.out.println(x); System.exit(1); } } @Override public void run() { while (b == null); while (true) f(b); } }.start(); for (int x = 0;;x++) b = new B(Math.max(x%2,1)); } } 

主线程

主线程创建B的实例,其中x设置为1,然后将该实例写入静态字段Ab 。 它永远重复这个动作。

投票线程

生成的线程轮询直到它发现Abx不是1。

?!?

有一半时间按预期进入无限循环,但有一半时间我得到这个输出:

 $ java A 0 

为什么轮询线程能够看到x未设置为1的B


x%2而不仅仅是x在这里只是因为问题可以用它重现。


我在linux x64上运行openjdk 6。

这就是我的想法:因为b不是最终的 ,编译器可以自由地重新排序操作,对吧? 因此,这基本上是一个重新排序问题,因此不安全的发布问题将变量标记为final将解决问题。

或多或少,它与Java内存模型文档中提供的示例相同。

真正的问题是这是如何可能的。 我也可以推测这里(因为我不知道编译器将如何重新排序),但是在写入x之前,可能会将对B的引用写入主内存(其他线程可见)。 在这两个操作之间,读取发生,因此为零值

围绕并发性的考虑通常集中在对状态或死锁的错误更改上。 但是,来自不同线程的状态的可见性同样重要。 现代计算机中有许多地方可以缓存状态。 在寄存器中,处理器上的L1缓存,处理器和内存之间的L2缓存等.JIT编译器和Java内存模型旨在尽可能利用缓存或合法,因为它可以加快速度。

它还可能产生意想不到的和违反直觉的结果。 我相信这种情况正在发生。

创建B实例时,实例变量x在设置为传递给构造函数的任何值之前会暂时设置为0。 在这种情况下,1。如果另一个线程试图读取x的值,即使x已经设置为1,它也可以看到值0.它可能看到一个陈旧的缓存值。

为了确保看到x的最新值,您可以执行以下操作。 你可以使x volatile,或者你可以在B实例上同步保护x的读取(例如,通过添加一个synchronized getX()方法)。 您甚至可以将x从int更改为java.util.concurrent.atomic.AtomicInteger

但到目前为止,纠正问题的最简单方法是使x最终。 无论如何,它在B的生命周期中永远不会改变。 Java为最终字段提供特殊保证,其中之一是在构造函数完成后,构造函数中的最终字段集对任何其他线程都是可见的。 也就是说,没有其他线程会看到该字段的陈旧值。

使字段不可变还有许多其他好处,但这是一个很好的。

另见Jeremy Manson的primefaces性,可见性和排序 。 特别是他所说的部分:

(注意:当我在这篇文章中说同步时,我实际上并不意味着锁定。我的意思是保证Java中的可见性或排序。这可以包括final和volatile字段,以及类初始化和线程启动和连接以及所有各种其他好东西。)

在我看来,Bx上可能存在竞争条件,因此可能存在分裂秒,其中Bx已经创建,并且在B的构造函数中此x = x之前Bx = 0。 一系列事件将是这样的:

 B is created (x defaults to 0) -> Constructor is ran -> this.x = x 

您的线程在创建之后但在构造函数运行之前的某个时间访问Bx。 但是,我无法在本地重新创建问题。