26

下面的示例来自 Brian Goetz 的《Java Concurrency in Practice》一书,第 3 章,第 3.5.1 节。这是对象发布不当的示例:

class SomeClass {
    public Holder holder;

    public void initialize() {
        holder = new Holder(42);
    }
}

public class Holder {
  private int n;
  public Holder(int n) { this.n = n; }

  public void assertSanity() {
    if (n != n)
      throw new AssertionError("This statement is false");
  }
}

它表示 Holder 可能会以不一致的状态出现在另一个线程中,并且另一个线程可以观察到部分构造的对象。这怎么可能发生?你能用上面的例子给出一个场景吗?

它还继续说,在某些情况下,线程第一次读取字段时可能会看到一个陈旧的值,然后下次读取一个更新的值,这就是assertSanitycan throw的原因AssertionError。怎么能AssertionError扔?

通过进一步阅读,解决此问题的一种方法是Holder通过将变量设为nfinal 来使其不可变。现在,让我们假设它Holder不是不可变的,但实际上是不可变的。

为了安全地发布这个对象,我们是否必须使持有者初始化为静态并将其声明为 volatile(静态初始化和 volatile 或只是 volatile)?

像这样的东西:

public class SomeClass {
    public static volatile Holder holder = new Holder(42);
}
4

4 回答 4

16

你可以想象一个对象的创建有许多非原子函数。首先你要初始化并发布 Holder。但是您还需要初始化所有私有成员字段并发布它们。

好吧,JMM 没有规定holder' 的成员字段的写入和发布在字段写入之前holder发生在initialize(). 这意味着即使holder不为空,成员字段对其他线程不可见也是合法的。

你最终可能会看到类似的东西

public class Holder {
    String someString = "foo";
    int someInt = 10;
}

holder可能不为空,但someString可以为空,someInt也可以为 0。

据我所知,在 x86 架构下,这是不可能发生的,但在其他架构中可能并非如此。

所以下一个问题可能是“为什么 volatile 解决了这个问题? ” JMM 说在 volatile 存储之前发生的所有写入对 volatile 字段的所有后续线程都是可见的。

因此,如果holder是 volatile 并且您看到holder的不是 null,则根据 volatile 规则,所有字段都将被初始化。

为了安全地发布这个对象,我们是否必须使持有者初始化为静态并将其声明为 volatile

是的,因为正如我提到的,如果holder变量不为空,那么所有写入都是可见的。

怎么能AssertionError扔?

如果一个线程注意到holder不为空,并且AssertionError在进入方法时调用并且n第一次读取可能是0(默认值),那么第二次读取n现在可能会看到来自第一个线程的写入。

于 2013-04-19T15:48:46.790 回答
3
public class Holder {
  private int n;
  public Holder(int n) { this.n = n; }

  public void assertSanity() {
    if (n!=n)
      throw new AssertionError("This statement is false");
  }
}

假设一个线程创建 的实例Holder,并将引用传递给另一个调用assertSanity.

构造函数中的赋值this.n发生在一个线程中。并且两次读取n发生在另一个线程中。这里唯一的发生前关系是两个读取之间的关系。没有涉及分配和任何读取的发生之前的关系。

没有任何happens-before关系,语句可以以各种方式重新排序,因此从一个线程的角度来看,this.n = n可以在构造函数返回之后发生。

这意味着赋值可能出现在第一次读取之后和第二次读取之前的第二个线程中,从而导致值不一致。可以通过设置nfinal 来防止这种情况,这保证了在构造函数完成之前分配了值。

于 2013-04-19T16:11:40.790 回答
0

您问的问题是由 JVM 优化和简单对象创建的事实引起的:

MyClass obj = new MyClass()

并不总是按步骤完成:

  1. 在堆上为 MyClass 的新实例保留内存
  2. 执行构造函数以设置内部属性值
  3. 将“obj”引用设置为堆上的地址

出于某些优化目的,JVM 可以通过以下步骤进行:

  1. 在堆上为 MyClass 的新实例保留内存
  2. 将“obj”引用设置为堆上的地址
  3. 执行构造函数以设置内部属性值

所以,想象一下如果两个线程想要访问 MyClass 对象。第一个创建它,但由于 JVM,它执行“优化”的一组步骤。如果它只执行第 1 步和第 2 步(但不会执行第 3 步),那么我们可能会遇到严重的问题。如果第二个线程使用这个对象(它不会为空,因为它已经指向堆上保留的内存部分),那么它的属性将不正确,这可能会导致令人讨厌的事情。

如果引用不稳定,则不会发生这种优化。

于 2016-09-13T13:59:22.737 回答
-1

该类Holder没问题,但该类someClass可能出现在不一致的状态 - 在创建和对initialize()实例holder变量的调用之间是null.

于 2013-04-19T16:00:54.560 回答