局部创建对象(不正确发布:当好对象变坏时)
代码清单1:
public class StuffIntoPublic { public Holder holder; public void initialize() { holder = new Holder(42); } }
代码清单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."); } }
你无法信赖局部创建对象,一个监视着处于不一致状态对象的线程,会看到尽管该对象自发布之后从未修改过,但是它的状态还是会发生突变。代码清单2的Holder使用代码清单1的不安全发布方式发布,那么除了发布线程,其他线程调用assertSanity时都可能抛出AssertionError异常。(此问题并非出在Holder类自身,而是Holder没有被正确的发布,但是将域n声明为final类型,使Holder成为不可变的,可以避免出现不正确发布的问题)
原因分析如下:
因为没有同步来确保Holder对其他线程可见,所以我们称Holder是“非正确发布的”,没有正确发布的对象会导致两种错误。首先,发布线程以外的任何线程都可以看到Holder域的过期值,因而看到的是一个null引用或者旧值,即使此刻Holder已经被赋与新值。其次,更坏的情况是,线程看到的Holder引用是最新的,然而Holder状态却是过期的。 (可以看出,在构造函数中设置的域值,应该是向这些域写入的第一个值,因此,没有“更旧”的值可以作为所谓的过期值。但是Object的构造函数会先于子类的构造函数运行,并首先向所有的域写入默认值。因此这些默认值可能成为域的过期值) 这使程序执行变得更加不可预测:线程首次读取某个域可能会看到过期值,再次读取该域会得到一个更新值,这正是assertSanity抛出AssertionError异常的原因。
不可变对象与初始化安全性
出于不可变对象的重要性,java存储模型为共享不可变对象提供了特殊的初始化安全性的保证。
另一方面,即使发布对象引用时没有使用同步,不可变对象仍然可以被安全地访问。为了获得这种初始化安全性的保证,应该满足所有不可变性的条件:不可修改的状态,所有域都是final类型的以及正确的构造。
不可变类的例子:
public class Holder { private final int n; public Holder(int n) { this.n = n; } public void assertSanity() { if (n != n) throw new AssertionError("This statement is false."); } }
注意:不可变对象可以在没有额外同步的情况下,安全地用于任意线程;甚至发布它们时也不需要同步。
这个保证还会延伸到一个正确创建的对象中的所有final类型域的值,没有额外的同步,final域也可以被安全地访问。然而,如果final域指向可变对象,那么访问这些对象的状态时仍然需要同步。
以上内容出 <<自并发编程实践>>