3.4 不可变性
为了满足同步的需要,另一种方法是使用不可变对象[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引用。
@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。
通过使用不可变对象来持有所有的变量,可以消除在访问和更新这些变量时的竞争条
@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没有显式地用到锁,但仍然是线程安全的。
分享到:
相关推荐
"String型的不可变性" Java 中的 String 型是一个特殊的包装类数据,它具有不可变性。什么是不可变性呢?简单来说,就是 String 对象一旦被创建,不能被修改。那么,为什么 String 对象不能被修改呢?这就需要从 ...
本文将深入探讨Java中String的不可变性,分析其设计原因,并讨论其优势和最佳实践。 Java中的String类是不可变的,这是由于其内部使用final字符数组实现的。这种设计带来了线程安全、内存优化和性能提升等优势,但也...
### Java常用类与基础API-String的理解与不可变性 #### 一、字符串相关类之不可变字符序列:String ##### (1) 对String类的理解(以JDK8为例说明) **1. 环境** 本文档使用JDK 8进行演示。JDK 8的环境设置确保了...
Java中的final关键字:不可变性与更多应用
计算机后端-Java-Java核心基础-第21章 常用类 06. 理解String的不可变性.avi
Java String 不可变性实现原理解析 Java String 不可变性是 Java 语言中 String 类的一个重要特性,它保证了 String 对象一旦创建就不能被修改,确保了多线程环境中的数据一致性和正确性。本文将详细介绍 Java ...
String的不可变性在很多场景下都是有益的,例如作为Map的键,因为在哈希表中,键的哈希码是不变的,String的不可变性保证了这一点。此外,字符串常量池的实现也依赖于String的不可变性,可以有效地复用相同的字符串...
"java String不可变性详解" Java 中的 String 类是一种不可变对象,它的不可变性是通过实例解析来实现的。下面我们将通过实例解析来详细介绍 Java 中 String 的不可变性。 一、不可变模式(Immutable Pattern) ...
现在,`built_redux`是这个理念在Dart语言中的实现,它同样强调了不可变性这一核心原则。 在Dart中,`built_redux`库为开发者提供了在Flutter或其他Dart应用中实现Redux架构的能力。不可变性是`built_redux`的一个...
在Java编程中,数据结构的不可变性是一个非常重要的概念。许多开发人员认为,只要使用final关键字或val关键字就可以使对象不可变,但是这是一种误解。不可变数据结构具有许多优势,例如没有无效的状态、线程安全、...
理解不可变性是非常重要的,无论是Java语言规范还是Java存储模型都没有对不可变性做出正式的定义。因此,需要开发者自己来确保对象的不可变性。例如,在定义不可变类时,需要将所有域都声明为final类型,并且确保...
在讨论不可变性时,文档也指出有时会需要可修改性。例如,在收集统计数据时,尤其是当数据可能来自不同的线程时,就需要线程安全的可修改集合。文档中提到了使用synchronizedMultimap来满足这样的需求。 总结来说,...
理解String对象的不可变性对于Java开发者来说至关重要,因为它影响着代码的编写、内存管理和多线程环境下的行为。 首先,不可变性意味着一旦一个String对象被创建,它的值就不能再被修改。例如: ```java String ...
了解其不可变性,有助于我们更好地理解和优化代码。以下是对这一核心概念的详细解释: 1. **不可变性定义**:不可变对象是指一旦创建,其状态就不能改变的对象。在Java中,`String`对象一旦被创建,它的值(字符...
不可变性意味着一旦创建了数据结构,就不能更改其内容。这种特性可以带来很多好处,例如更容易的并发处理、更好的性能优化以及更简单的调试。在JavaScript中,虽然原始类型(如字符串、数字和布尔值)本身就是不可变...
不可变性保证了数据的安全性,因为它们不会因为意外的修改而产生副作用,特别适用于多线程或并发环境。 2. **Python中的可变与不可变容器**: Python的列表、字典等是可变容器,允许添加、删除或更改元素。而元组...
不可变性意味着一旦创建一个对象,其状态就不能改变。在`immutable.js`中,对数据的任何操作都会返回一个新的对象,而不是直接修改原有对象。这种特性确保了代码的预测性,减少了因意外修改导致的错误,并且有利于...
然而,如果需要频繁地添加、删除或修改元素,列表则更适合,因为元组的不可变性会使得这些操作变得复杂且效率低。 在实际应用中,根据具体需求选择合适的数据结构非常重要。例如,在定义函数返回值时,如果返回值不...
不可变对象由于其不可变性,更易于进行引用计数,而可变对象可能涉及更复杂的引用关系,如循环引用,需要更复杂的垃圾回收策略。 总的来说,了解Python中的可变与不可变数据类型及其特性,可以帮助我们更好地控制...