让我们看看这个简单的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
为什么轮询线程能够看到B
, x
没有设置为1?
x%2
而不是x
在这里只是因为问题是可重复的。
我在linux x64上运行openjdk 6
这是我的想法:因为b不是final,编译器可以自由地重新排序操作,因为它喜欢,对吗?因此,从根本上说,这是一个重新排序问题,因此不安全的发布问题将变量标记为final将解决这个问题。
或多或少,它与Java内存模型文档中提供的示例相同。
真正的问题是这怎么可能。我也可以在这里推测(因为我不知道编译器将如何重新排序),但也许在写x发生之前,对B的引用被写入主内存(对其他线程可见)。在这两个操作之间发生读操作,因此零值
围绕并发性的考虑通常集中在对状态或死锁的错误更改上。但是来自不同线程的状态可见性同样重要。在现代计算机中有许多地方可以缓存状态。在寄存器中,处理器上的L1缓存,处理器和存储器之间的L2缓存,等等。JIT编译器和Java内存模型被设计成尽可能或合法地利用缓存,因为它可以加快速度。
它也可以给出意想不到的和违反直觉的结果。我相信这种情况正在发生。
当创建B的实例时,在将实例变量x设置为传递给构造函数的任何值之前,它被短暂地设置为0。在这种情况下,1。如果另一个线程试图读取x的值,即使x已经被设置为1,它也可以看到值0。它可能看到一个过时的缓存值。
要确保看到x的最新值,您可以做几件事。您可以使x易失性,或者您可以在B实例上通过同步保护x的读(例如,通过添加synchronized getX()
方法)。您甚至可以将x从int类型更改为java.util.concurrent.atomic.AtomicInteger
类型。
但到目前为止,纠正这个问题最简单的方法是使x为终值。在B的生命周期中它永远不会改变。Java对final字段做了特殊保证,其中之一是在构造函数完成后,构造函数中设置的final字段对任何其他线程都是可见的。也就是说,其他线程不会看到该字段的过期值。
使字段不可变还有许多其他好处,但这是一个很大的好处。
参见Jeremy Manson的《原子性、可见性和排序》。尤其是他说:
(注意:当我在这篇文章中说同步时,我实际上并不是指锁定。我指的是任何在Java中保证可见性或排序的东西。这可以包括final和volatile字段,以及类初始化,线程启动和连接以及各种其他好东西。
在我看来,B.x上可能存在竞争条件,因此可能存在B.x已经创建并且B.x=0的瞬间。在B的构造函数中x = x。事件序列类似于:
B is created (x defaults to 0) -> Constructor is ran -> this.x = x
你的线程在B.x创建之后的某个时间访问B.x,但在构造函数运行之前。但是,我无法在本地重新创建此问题。