让我们来看看这个简单的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,然后写入该实例与静态磁场Ab
。 它永远重复这个动作。
轮询线程
直到它找到了产生的线程民调Abx
不是1。
?!?
一半的时间在推移预期无限循环,但一半的时间我得到这样的输出:
$ java A
0
为什么轮询线程能够看到B
已x
未设置为1?
x%2
,而不是仅仅x
在这里仅仅是因为这个问题是可重复使用它。
我在Linux上运行64位的OpenJDK 6。
以下是我认为:由于B是不是终点 ,编译器可以自由地重新排序操作,因为它喜欢的,对不对? 所以这一点,从根本上是一个重新排序的问题,作为结果的不安全的出版物问题的标记变量作为最终将解决这个问题。
更多或更少,这是因为在这里所提供的相同的例子Java内存模型文档 。
真正的问题是,这怎么可能。 我也可以推测这里(因为我不知道该编译器将如何重新排序),但也许到B的参考写入到主内存(如果是可见的其他线程)在写之前,X发生。 在这两个操作读之间发生,从而零值
常的考虑周围并发重点放在错误变为状态或死锁。 但是,从不同的线程状态的可见性也同样重要。 有在现代计算机的许多地方,状态可以被缓存。 在寄存器,在处理器上的L1高速缓存中,处理器和存储器等的JIT编译器和Java存储器模型之间的L2高速缓存被设计为采取缓存优点尽可能或法律,因为它可以加快速度。
它也可以给不合常理的结果。 我认为,在这种情况下发生的。
当创建B的一个实例,该实例变量x被简要地被设置为被传递到构造的任何值之前设置为0。 在这种情况下,1。如果另一个线程试图读取x的值,它可以看到值0,即使X已经被设置为1,这可能会看到一个陈旧的缓存值。
为了确保x的跟上时代的价值所看到的,有几件事情可以做。 你可以做X挥发,或者你可以(例如,通过添加一个保护X与在B实例同步读取synchronized getX()
方法)。 你甚至可以从一个int到改变X java.util.concurrent.atomic.AtomicInteger
。
但到目前为止,以纠正这个问题最简单的办法就是让X决赛。 它是永远不会反正B的寿命期间改变。 Java使得特别保证最终场,其中之一是在构造完成后,在构造函数中最后的字段设置将是可见的任何其他线程。 也就是说,没有其他线程将看到该字段的值陈旧。
制作领域不可改变有许多其他好处,但是这是一个伟大的。
另请参见原子性,可见性和顺序由杰里米·曼森。 特别是部分在那里,他说:
(注意:当我说在这个岗位同步,我实际上并不意味着锁定我的意思是什么,保证在Java中的知名度或订购这可以包括最终震荡领域,以及类的初始化和线程启动并加入所有各种各样的其他好东西。)
在我看来,有可能是在Bx的竞争条件,例如,有可能存在第二次分裂Bx的已经建立并在Bx的= 0先于B的构造函数中this.x = X。 该系列活动的会是这样的:
B is created (x defaults to 0) -> Constructor is ran -> this.x = x
你的线程访问Bx的被创建之后的某个时间,但构造跑了。 我无法在本地重现问题,但是。
文章来源: Uninitialized object leaked to another thread despite no code explicitly leaking it?