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()
并不总是按步骤完成:
- 为Heap上的MyClass新实例保留内存
- 执行构造函数以设置内部属性值
- 将’obj’引用设置为堆上的地址
出于某些优化目的,JVM可以通过以下步骤完成:
- 为Heap上的MyClass新实例保留内存
- 将’obj’引用设置为堆上的地址
- 执行构造函数以设置内部属性值
因此,假设两个线程想要访问MyClass对象。 第一个创建它但由于JVM它执行“优化”步骤集。 如果它只执行步骤1和2(但不会执行3),那么我们就会遇到严重的问题。 如果第二个线程使用这个对象(它不会为null,因为它已经指向堆上的内存的保留部分),它的属性将是不正确的,这可能导致令人讨厌的事情。
如果引用将是不稳定的,则不会发生这种优化。
Holder
类是正常的,但someClass
类可以出现在一个不一致的状态 – 在创建和initialize()
的调用之间initialize()
holder
实例变量为null
。