java并发编程实践 3.4 不可变性

缘起

《java并发编程实践》 3.4 不可变性

分析

不可变对象永远是线程安全的,因为它消除了竞争条件. 所以正如”将所有的属性声明为private的,除非他们需要更高的可见性”一样,”将所有的属性声明为final的,除非他们是可变的”.

​ —- Effective Java

首先,什么对象是不可变的? 并不是所有域都加上final修饰就是不可变的. 因为所有域都是final的对象依旧可能是可变的, 因为final域可以获取到一个可变对象的引用

1
2
3
4
只有满足以下条件,一个对象才能被叫做是不可变的
1. 它的状态(即属性)在创建之后就不能被修改
2. 所有域都是final
3. 被正确创建(即在创建期间没有发生this引用的逸出)

举个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Immutable
public class ThreeStooges {
private final Set<String> stooges = new HashSet<String>();

public ThreeStooges() {
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}

public boolean isStooges(String name) {
return stooges.contains(name);
}
}

这就是一个不可变的对象.

final关键字表示不可变的(final修饰class,修饰方法,修饰属性,其中修饰属性又分基本类型变量和引用类型变量,这些情形下final的含义属于java基础. 不赘述).除此之外,在JMM中含有特殊的语义——final属性是初始化安全的——即让不可变的属性不需要同步就能自由的被访问和共享. 说的有点抽象,具体看下面的例子 (使用volatile关键字安全发布不可变对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Immutable
class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors; // lastFactors是lastNumber的分解质因数的结果

public OneValueCache(BigInteger i, BigInteger[] factors) {
lastNumber = i;
lastFactors = Arrays.copyOf(factors, factors.length);
}

public BigInteger[] getFactors(BigInteger i) { // 获取i的分解质因数的结果
if(lastNumber==null || !lastNumber.equals(i)) { // 如果缓存中没有, 则返回null
return null;
}
return Arrays.copyOf(lastFactors, lastFactors.length); // 如果有的话, 就直接返回缓存的结果
}
}

@ThreadSafe
public class VolatileCachedFactorizer implement Servlet {
private volatile OneValueCache cache = new OneValueCache(null, null);

public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = cache.getFactors(i); // 试图从缓存中拿
if(factors == null) { // 缓存中未取到
factors = factor(i); // 将i分解质因数
cache = new OneValueCache(i, factors); // 重新缓存
}
encodeIntoResponse(resp, factors); // 响应客户端
}
}

注意,上面的OneValueCache是不变的容器, 而VolatileCachedFactorizer是线程安全的servlet. 即假使有多根线程利用该servlet的实例干活, OneValueCache的两个不可变的属性只有在初始化的时候被赋值. 而final关键字的JMM语义保证了线程安全性(final的JMM语义是保证线程安全的关键,而不是volatile关键字.volatile关键字只是保障了某种线程通信的及时性). 所以这里不会出现 待分解的数字和质因数数组不匹配(即质因数数组的乘积!=待分解的数字 的情形). 也就保证了线程安全性. 而volatile语义使得前一根线程的修改(第28行)能马上被别的线程看见. 虽然完全可能之前的线程和原先的OneValueCache打交道,但是这些线程仍将看到一致的状态.即线程是安全的

至此,上述缓存是线程安全的.