为什么代码会面临看到部分构造的对象的风险?

Why the code would be at risk for seeing a partially constructed object?

关于在ibm中使用volatile的article,解释让我很困惑,下面是本文中的示例及其解释:

public class BackgroundFloobleLoader {
    public volatile Flooble theFlooble;

    public void initInBackground() {
        // do lots of stuff
        theFlooble = new Flooble();  // this is the only write to theFlooble
    }
}

public class SomeOtherClass {
    public void doWork() {
        while (true) { 
            // do some stuff...
            // use the Flooble, but only if it is ready
            if (floobleLoader.theFlooble != null) 
                doSomething(floobleLoader.theFlooble);
        }
    }
}

Without the theFlooble reference being volatile, the code in doWork() would be at risk for seeing a partially constructed Flooble as it dereferences the theFlooble reference.

这个怎么理解?为什么没有volatile,我们可以使用部分构造的Flooble对象?谢谢!

当不同的线程访问你的代码时,任何线程都可以对你的对象的状态进行修改,这意味着当其他线程访问它时,状态可能不是它应该的状态。

来自 oracle 文档:

The Java programming language allows threads to access shared variables. As a rule, to ensure that shared variables are consistently and reliably updated, a thread should ensure that it has exclusive use of such variables by obtaining a lock that, conventionally, enforces mutual exclusion for those shared variables.

The Java programming language provides a second mechanism, volatile fields, that is more convenient than locking for some purposes.

A field may be declared volatile, in which case the Java Memory Model ensures that all threads see a consistent value for the variable.

source

也就是说这个变量的值永远不会被缓存thread-locally,所有的读写都会直接到"main memory"

例如图片thread1和thread2访问对象:

  1. Thread1 访问对象并将其存储在其本地缓存中
  2. Trhead2 修改对象
  3. 线程 1 再次访问该对象,但由于它仍在其缓存中,因此它不会访问线程 2 更新后的状态。

我怀疑 Java 中是否存在部分构造的对象。 Volatile 保证每个线程都会看到一个构造的对象。由于 volatile 在引用的对象上像一个微小的同步块一样工作,如果 theFlobble == null,你最终会得到一个 NPE。也许这就是他们的意思。

对象封装了很多东西:变量、方法等,这些都需要时间才能在计算机中出现。在 Java 中,如果任何变量被声明为 volatile,那么所有对它的读写都是原子的。因此,如果引用对象的变量被声明为 volatile,则只有当它完全加载到您的系统中时才允许访问其成员(您如何读取或写入根本不存在的东西?)

如果没有 volatile,您可能会看到一个部分构建的对象。例如。考虑这个 Flooble 对象。

public class Flooble {
    public int x;
    public int y;

    public Flooble() {
       x = 5;
       y = 1;
    }
}

public class SomeOtherClass {
    public void doWork() {
    while (true) { 
        // do some stuff...
        // use the Flooble, but only if it is ready
        if (floobleLoader.theFlooble != null) 
            doSomething(floobleLoader.theFlooble);
    }

    public void doSomething(Flooble flooble) {
        System.out.println(flooble.x / flooble.y);
    }
}

}

如果没有 volatile,doSomething 方法不能保证看到 xy 的值 51。例如,它可以看到 x == 5y == 0,导致被零除。

执行此操作时theFlooble = new Flooble(),发生3次写入:

  1. tmpFlooble.x = 5
  2. tmpFlooble.y = 1
  3. theFlooble = tmpFlooble

如果这些写入按此顺序发生,则一切正常。但是如果没有 volatile ,编译器可以自由地重新排序这些写入并按需要执行它们。例如。首先是第 3 点,然后是第 1 点和第 2 点。

这实际上一直都在发生。编译器确实会重新排序写入。这样做是为了提高性能。

错误很容易通过以下方式发生:

线程 A 从 class BackgroundFloobleLoader 执行 initInBackground() 方法。编译器重新排序写入,因此在执行 Flooble() 的主体(其中设置了 xy)之前,线程 A 首先执行 theFlooble = new Flooble()。现在,theFlooble 指向一个 flooble 实例,其 xy0。在线程 A 继续之前,其他线程 B 执行了 class SomeOtherClass 的方法 doWork()。此方法使用 theFlooble 的当前值调用方法 doSomething(floobleLoader.theFlooble)。在此方法中,theFlooble.x 除以 theFlooble.y,结果除以零。线程 B 由于未捕获的异常而结束。线程 A 继续并设置 theFlooble.x = 5theFlooble.y = 1.

这种情况当然不会发生在每个运行,但是根据Java的规则,可以发生。

从执行此操作的代码的角度来看:

        if (floobleLoader.theFlooble != null) 
            doSomething(floobleLoader.theFlooble);

显然,在 theFlooble 可能测试为 != null 之前,您需要保证 new Flooble() 执行的所有写入对该代码可见。没有 volatile 的代码中没有任何内容提供这种保证。所以你需要一个你没有的保证。失败。

Java 提供了几种方法来获得您需要的保证。一种是使用 volatile 变量:

... any write to a volatile variable establishes a happens-before relationship with subsequent reads of that same variable. This means that changes to a volatile variable are always visible to other threads. What's more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change. -- Docs

因此,在一个线程中写入 volatile 并在另一个线程中读取 volatile 恰好建立了我们需要的 happens-before 关系。