Java Object Reference的发布不正确

下面的例子来自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可能出现在另一个处于不一致状态的线程中,而另一个线程可以观察到部分构造的对象。 怎么会发生这种情况? 你能用上面的例子给出一个场景吗?

此外,它继续说有些情况,当一个线程第一次读取一个字段时可能会看到一个陈旧的值,然后下次再看到一个更新的值,这就是assertSanity可以抛出断言错误的原因。 如何抛出assertionError?

从进一步阅读,解决这个问题的一种方法是通过使变量’n’最终使Holder不可变。 现在,让我们假设持有人不是无法忍受的,但实际上是不可改变的。 为了安全地发布这个对象,我们是否必须将holder初始化设置为static并将其声明为volatile(静态初始化和volatile或者只是volatile)? 就像是

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

感谢您的帮助。

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

那么JMM没有规则来写holder成员字段的写入和发布 – 在写入holder字段之前发生在initialie() 。 这意味着即使holder不为null,对于其他线程尚不可见的成员字段也是合法的。

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

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

holder可能不是null,但someString可以为null, someInt可以为0。

在x86拱门下,据我所知,这是不可能发生的,但在其他情况下可能并非如此。

所以下一个问题可能是Why does volatile fix this? JMM表示在易失性存储之前发生的所有写操作都对volatile字段的所有后续线程可见。

因此,如果holder是易变的并且您看到holder不为空,则基于易变规则,所有字段都将被初始化。

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

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

如何抛出assertionError?

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

 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读取。 这里唯一发生的关系是两次读取之间的关系。 没有发生在涉及赋值和任何读取的关系之前。

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

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

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

 MyClass obj = new MyClass() 

并不总是按步骤完成:

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

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

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

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

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

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