Java对象引用的不正确发布

Java对象引用的不正确发布,java,concurrency,Java,Concurrency,下面的示例摘自Brian Goetz的《Java并发实践》一书,第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 =

下面的示例摘自Brian Goetz的《Java并发实践》一书,第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");
  }
}
它说,持有者可能在不一致的状态下出现在另一个线程中,而另一个线程可能观察到一个部分构造的对象。这怎么会发生?你能用上面的例子给出一个场景吗

此外,它还指出,在某些情况下,线程可能会在第一次读取字段时看到一个过时的值,然后在下一次看到一个更为最新的值,这就是
assertSanity
可以抛出
AssertionError
的原因。如何抛出断言错误

通过进一步阅读,解决此问题的一种方法是使变量
n
为final,从而使
Holder
不可变。现在,让我们假设
Holder
不是不可变的,而是实际上是不可变的

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

大概是这样的:

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

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

嗯,JMM没有规则规定在写入
holder
字段之前写入和发布
holder
的成员字段,就像发生在
initialize()
中一样。这意味着,即使
holder
不为null,其他线程仍不能看到成员字段是合法的

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

public class Holder {
    String someString = "foo";
    int someInt = 10;
}
holder
不能为null,但是
someString
可以为null,
someInt
可以为0

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

因此,下一个问题可能是“volatile为什么会解决这个问题?”JMM说,volatile存储之前发生的所有写操作对volatile字段的所有后续线程都是可见的

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

要安全地发布此对象,我们是否必须 初始化为静态,并将其声明为volatile

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

如何抛出断言错误


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

holder
类正常,但是类
someClass
可能在创建和调用
initialize()
之间以不一致的状态出现
holder
实例变量为
null

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
。这里唯一的before关系是两个read之间的关系。不存在涉及赋值和任何读取的before关系

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


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

您所问的问题是由JVM优化和简单对象创建:

MyClass obj = new MyClass()
并非总是按步骤完成:

  • 为堆上MyClass的新实例保留内存
  • 执行构造函数以设置内部属性值
  • 将“obj”引用设置为堆上的地址
  • 出于某些优化目的,JVM可以通过以下步骤完成:

  • 为堆上MyClass的新实例保留内存
  • 将“obj”引用设置为堆上的地址
  • 执行构造函数以设置内部属性值
  • 所以,想象一下,如果两个线程想要访问MyClass对象。第一个创建它,但由于JVM,它执行“优化”的步骤集。如果它只执行第1步和第2步(但不执行第3步),那么我们可能会遇到严重的问题。如果第二个线程使用这个对象(它不会为null,因为它已经指向堆上的内存保留部分),那么它的属性将不正确,这可能会导致糟糕的事情


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

    我所能看到的是,在多处理器情况下,两个处理器之间的缓存状态可能不一致。在非紧密耦合的MP环境中,这始终是一种可能性,除非您采取明确的步骤进行同步。@PaulGrime-在简要回顾之后,我没有看到任何解决上述场景的方法。在构造对象之前,引用不是“转义”。int
    n
    不是公共的,不能在类外查看。不是,但是
    holder
    是公共的,另一个线程可以在其构造函数和字段初始化完成之前调用它的
    assertSanity
    。也许-.@PaulGrime-在构造函数返回之前(如果JITC兼容),对已创建对象的引用不会分配给
    holder