4

Let's see this simple Java program:

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));
    }
}

Main thread

The main thread creates an instance of B with x set to 1, then writes that instance to the static field A.b. It repeats this action forever.

Polling thread

The spawned thread polls until it finds that A.b.x is not 1.

?!?

Half the time it goes in an infinite loop as expected, but half the time I get this output:

$ java A
0

Why is the polling thread able to see a B that has x not set to 1?


x%2 instead of just x is here simply because the issue is reproducible with it.


I'm running openjdk 6 on linux x64.

4

3 回答 3

10

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

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

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

于 2013-04-24T11:02:50.177 回答
1

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

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

创建 B 的实例时,实例变量 x 在被设置为传递给构造函数的任何值之前被短暂设置为 0。在本例中为 1。如果另一个线程尝试读取 x 的值,即使 x 已经设置为 1,它也可能看到值 0。它可能看到的是一个陈旧的缓存值。

为了确保看到 x 的最新值,您可以做几件事。您可以使 x 易失性,或者您可以通过 B 实例上的同步来保护 x 的读取(例如,通过添加一个synchronized getX()方法)。您甚至可以将 x 从 int 更改为 a java.util.concurrent.atomic.AtomicInteger

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

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

另请参阅Jeremy Manson 的原子性、可见性和排序。特别是他说的部分:

(注意:当我在这篇文章中说同步时,我实际上并不是指锁定。我指的是在 Java 中保证可见性或顺序的任何东西。这可以包括 final 和 volatile 字段,以及类初始化和线程启动和连接等等其他好东西。)
于 2013-04-23T20:24:09.480 回答
0

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

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

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

于 2013-04-23T20:09:01.500 回答