以下示例来自Brian Goetz,第3章,第3.5.1节中的“Java Concurrency in Practice”一书。这是不正确的对象发布的一个例子
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初始化为静态并将其声明为volatile(静态初始化和volatile或者只是volatile)?像
这样的东西public class someClass {
public static volatile Holder holder = new Holder(42);
}
提前感谢您的帮助。
答案 0 :(得分:12)
你可以想象一个对象的创建有许多非原子功能。首先,您要初始化并发布Holder。但是您还需要初始化所有私有成员字段并发布它们。
在holder
中写holder
字段之前,JMM没有编写和发布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列表之前发生的所有写操作都对volatile字段的所有后续线程都可见。
因此,如果holder
是易变的,并且您看到holder
不为空,则根据易变规则,所有字段都将被初始化。
为了安全地发布此对象,我们是否必须制作持有者 初始化静态并将其声明为volatile
是的,因为正如我所提到的,holder
变量不是null,那么所有的写入都是可见的。
如何抛出assertionError?
如果一个帖子注意到holder
不为空,并在输入方法时调用assertionError
并且第一次阅读n
可能是0
(默认值) ,n
的第二次读取现在可以看到第一个帖子的写入。
答案 1 :(得分:2)
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来防止,这可以保证在构造函数完成之前分配值。
答案 2 :(得分:0)
您询问的问题是由JVM优化和简单对象创建引起的:
MyClass obj = new MyClass()
并不总是按步骤完成:
出于某些优化目的,JVM可以通过以下步骤完成:
因此,想象一下两个线程是否想要访问MyClass对象。第一个创建它但由于JVM它执行'优化'步骤集。如果它只执行步骤1和2(但不会执行3),那么我们就会遇到严重的问题。如果第二个线程使用这个对象(它不会为null,因为它已经指向堆上的内存的保留部分),它的属性将是不正确的,这可能导致令人讨厌的事情。
如果引用不稳定,则不会发生这种优化。
答案 3 :(得分:-1)
Holder
类没问题,但是类someClass
可能会出现不一致状态 - 在创建和调用initialize()
之间holder
实例变量为{{ 1}}。