Java 未初始化的对象泄漏到另一个线程,尽管没有代码显式泄漏它?

Java 未初始化的对象泄漏到另一个线程,尽管没有代码显式泄漏它?,java,concurrency,initialization,memory-visibility,Java,Concurrency,Initialization,Memory Visibility,让我们看看这个简单的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

让我们看看这个简单的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 = q.x;
                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,然后将该实例写入静态字段
A.B
。它永远重复这个动作

轮询线程

生成的线程轮询,直到它发现
A.b.x
不是1

有一半的时间它像预期的那样进入无限循环,但有一半的时间我得到这个输出:

$ java A
0
为什么轮询线程能够看到未将
x
设置为1的
B


x%2
而不仅仅是
x
出现在这里,原因很简单,因为问题可以通过它重现



我在linux x64上运行openjdk 6。

在我看来,B.x上可能存在竞争条件,因此在B的构造函数中,在创建了B.x且B.x=0之前,可能存在一个瞬间。这一系列事件将类似于:

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

您的线程在创建B.x之后但在构造函数运行之前访问它。但是,我无法在本地重新创建问题。

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

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


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

要确保看到x的最新值,可以执行以下几项操作。您可以使x易失性,也可以通过在B实例上进行同步来保护x的读取(例如,通过添加
synchronized getX()
方法)。您甚至可以将x从int更改为
java.util.concurrent.atomic.AtomicInteger

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

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

另见杰里米·曼森。特别是他说:


(注意:当我在这篇文章中提到同步时,我实际上并不是指锁定。我指的是保证Java可视性或顺序的任何东西。这可以包括final和volatile字段,以及类初始化、线程启动和连接以及其他各种好东西。)以下是我的想法:因为b不是final,编译器可以随意对操作重新排序,对吗?因此,这从根本上说是一个重新排序问题,因此,将变量标记为final的不安全发布问题将解决问题

或多或少,它与中提供的示例相同


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

您是说如果没有
synchronized(this){}
,或者使用
Math.max(x,1)
,您就不能复制这个结果吗?您已经独立地尝试了其中的每一个?在类B中,将实例变量
设为final
,然后看看会发生什么。您正在观察
b.x
的值,它处于创建和设置为传递给构造函数的值之间。@DavidConrad:但他不应该这样。静态
b
的赋值应该在构造函数完成之后才会发生。啊,对了。这是一个可见性问题。更新后的x值在另一个线程中不可见。不过,
final
的一个保证是,该值对任何其他线程都是可见的。如何?调用构造函数,并且只有在完成之后,才会将结果实例分配给静态
b
。您的意思是在构造函数运行之前,
newb(…)
的结果被分配给静态
B
。这将是非同寻常的,非同寻常的主张需要非同寻常的证据(或者在本案中,需要参考文献)。:-)不,一开始我也是这么想的。发生的情况是,分配对象,调用构造函数,x为0,该值缓存在某个寄存器或CPU缓存线中,x设置为1,构造函数完成,另一个线程尝试读取x,并获取过时的缓存值。将x设为final、volatile、AtomicInteger或在B实例上通过同步保护读取将纠正它。问题是:首先会发生什么,赋值还是构造函数?我怀疑这是一个“漏洞”,所以我能找到的唯一解释,尽管与我的直觉相反,是构造函数发生在赋值之后。也许这是JVM试图进行的某种优化。@DavidConrad:这至少有一些逻辑。我不喜欢它,觉得很难相信(我目前没有一个系统可以尝试,我正在路上),但有一些逻辑。