Java对象引用的不正确发布
下面的示例摘自Brian Goetz的《Java并发实践》一书,第3章,第3.5.1节。这是对象发布不当的一个示例: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 =
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()
并非总是按步骤完成:
如果引用是不稳定的,这种优化就不会发生。我所能看到的是,在多处理器情况下,两个处理器之间的缓存状态可能不一致。在非紧密耦合的MP环境中,这始终是一种可能性,除非您采取明确的步骤进行同步。@PaulGrime-在简要回顾之后,我没有看到任何解决上述场景的方法。在构造对象之前,引用不是“转义”。int
n
不是公共的,不能在类外查看。不是,但是holder
是公共的,另一个线程可以在其构造函数和字段初始化完成之前调用它的assertSanity
。也许-.@PaulGrime-在构造函数返回之前(如果JITC兼容),对已创建对象的引用不会分配给holder