不可变性
为了满足同步的需要,另一种方法是使用不可变对象[EJ Item13]。到目前为止,几乎所有我们已经描述过的原子性与可见性的危险,比如访问过期数据,未及时更新或者观察一个处于不一致状态的对象,它们都产生于多线程下各种难以预测的行为协同工作,多个线程总试图同时访问相同的可变状态。如果对象的状态不能被修改,这些风险与复杂度就自然而然地消失了。
创建后状态不能被修改的对象叫做不可变对象。不可变对象天生是线程安全的。它们的常量(域)是在构造函数中创建的。既然它们的状态无法被修改,这些常量永远不会变。
不可变对象永远是线程安全的。
|
不可变对象是简单的。它们只有一种状态,构造函数谨慎地控制着这个状态。程序设计中最困难的元素之一是推断复杂对象的可能状态。另一方面,推断不可变对象的状态却是很轻松的。
不可变对象也是更安全的。将可变对象传递给不可信的代码,或者将它发布到不可信代码可以找到的地方,都是危险的——不可信代码可能会改变它们的状态,甚至更糟的,还会保留引用并在其他线程中修改它们的状态。另一方面,不可变对象不会被恶意的或者
漏洞百出的代码所破坏,所以它们是安全的,可以放心地共享和发布,不需要创建防御性拷贝 [EJ Item 24]。
无论是Java语言规范还是Java存储模型都没有关于不可变性的正式定义,但是不可变性并不简单地等于将对象中的所有域都声明为final类型,所有域都是final类型的对象仍然可以是可变的,因为final域可以获得一个到可变对象的引用。
只有满足如下状态,一个对象才是不可变的:
l 它的状态不能在创建后再被修改;
l 所有域都是final类型13;并且,
l 它被正确创建(创建期间没有发生this引用的逸出)。
|
在不可变对象的内部,同样可以使用可变性对象来管理它们的状态,如同清单3.11中示范的那样。尽管存储姓名的set是可变的,但是ThreeStooges的设计使得它在被创建后就不可能再修改set。Stooges引用是final类型的,所以所有的对象状态只能通过final域询问。前面列出的最后一条要求,“正确创建”是容易满足的。因为构造函数不会做什么事情,从而引起this的调用者之外的和不同于构造函数的代码也可以访问this引用。
清单3.11 构造于底层可变对象之上的不可变类
@Immutable
public final class ThreeStooges {
private final Set<String> stooges = new HashSet<String>();
public ThreeStooges() {
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}
public boolean isStooge(String name) {
return stooges.contains(name);
}
}
因为程序的状态自始至终都在变化着,你可能会想使用不可变对象会有很多限制,但情况并非如此。“对象是不可变的”与“到对象的引用是不可变的”之间并不等同。
程序存储在不可变对象中的状态仍然可以通过替换一个带有新状态的不可变对象的实例得到更新。后面的章节提供了使用这项技术的例子13。
3.4.1 Final域
final关键字源于C++的const机制,不过受到了更多的限制。它对不可变性对象的创建提供了支持。final域是不能修改的(尽管如果final域指向的对象是可变的,这个对象仍然可被修改),然而它在Java存储模型中还有着特殊的语义。final域使得确保初始化安全性(initialization safety)成为可能,初始化安全性让不可变性对象不需要同步就能自由地被访问和共享。
即使对象是可变的,将一些域声明为final类型仍然有助于简化对其状态的判断。因为限制了对象的可见性,也就约束了其可能的状态集,即使有一两个可变的状态变量,这样一个“几乎不可变”的对象仍然比有很多的可变变量的对象要简单。将域声明为final类型,还向维护人员明确指出这些域是不能变的。
正如“将所有的域声明为私有的,除非它们需要更高的可见性”[EJ Item 12] 一样,“将所有的域声明为final型,除非它们是可变的”,也是一条良好的实践。
|
3.4.2 示例:使用volatile发布不可变对象
在第24页的UnsafeCachingFactorizer中,我们试图用两个AtomicReference存储最新的数字和它的因数。但是这并非是线程安全的,因为我们无法原子化地获取或者更新这两个相关的值。由于同样的原因,使用volatile变量也是不能保证线程安全性的。但是,有时不可变对象也可以提供一种弱形式的原子性。
用于因式分解的Servlet执行两个必须是原子性的操作:更新缓冲的结果,以及根据缓冲数值是否与请求数值相匹配,有条件的选取缓存中的结果。无论何时,对一组相关数值都应该执行原子性操作,并且可以考虑为它们创建不可变的容器(holder)类,比如清单3.12中的OneValueCache14。
通过使用不可变对象来持有所有的变量,可以消除在访问和更新这些变量时的竞争条
清单3.12 在不可变的容器中缓存数字和它的因数
@Immutable
class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
public OneValueCache(BigInteger i,
BigInteger[] factors) {
lastNumber = i;
lastFactors = Arrays.copyOf(factors, factors.length);
}
public BigInteger[] getFactors(BigInteger i) {
if (lastNumber == null || !lastNumber.equals(i))
return null;
else
return Arrays.copyOf(lastFactors, lastFactors.length);
}
}
件。若使用可变的容器对象,你就必须使用锁以确保原子性;使用不可变对象,一旦一个线程获得了它的引用,永远不必担心其他线程会修改它的状态。如果更新变量,会创建新的容器对象,不过在此之前任何线程都还和原先的容器打交道,仍然看到它处于一致的状态。
清单3.13的VolatileCachedFactorizer利用OneValueCache存储缓存的数字及其因数。当一个线程设置volatile类型的cache域引用到一个新的OneValueCache后,新数据会立即对其他线程可见。
与cache域相关的操作不会相互干扰,因为OneValueCache是不可变的,而且每次只有一条相应的代码路径访问它。不可变的容器对象持有与不变约束相关的多个状态变量,并利用volatile引用确保及时的可见性,这两个前提保证了即使VolatileCachedFactor没有显式地用到锁,但仍然是线程安全的。