在前面的UnsafeCachingFactorizer类中,我们尝试用两个AtomicReferences变量来保存最新的数值及其因数分解结果,但这种方式并非是线程安全的,因为我们无法以原子方式来同时读取或更新这两个相关的值。同样,用volatile类型的变量来保存这些值也不是线程安全的。然而,在某些情况下,不可变对象能提供一种弱形式的原子性。
因式分解Servlet将执行两个原子操作:更新缓存的结果,以及通过判断缓存中的数值是否等于请求的数值来决定是否直接读取缓存中的因数分解结果。每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据,例如程序清单3-12中的OneValueCache。
@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时,其他线程就会立即看到新缓存的数据。
@ThreadSafe
public class VolatileCachedFactorizer implements 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) {
factorfactors = factor(i);
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(resp, factors);
}
}
与cache相关的操作不会相互干扰,因为OneValueCache是不可变的,并且在每条相应的代码路径中只会访问它一次。
通过使用包含多个状态变量的容器对象来维持不变性条件,并使用一个volatile类型的引用来确保可见性,使得VolatileCachedFactorizer在没有显式地使用锁的情况下仍然是线程安全的。
程序清单3-13中存在『先检查后执行』(Check-Then-Act)的竞态条件。
OneValueCache类的不可变性仅保证了对象的原子性。
volatile仅保证可见性,无法保证线程安全性。
综上,对象的不可变性+volatile可见性,并不能解决竞态条件的并发问题,所以原文的这段结论是错误的。
疑惑已经解决了。
结论:
cache对象在service()中只有一处写操作(创建新的cache对象),其余都是读操作,这里符合volatile的应用场景,确保cache对象对其他线程的可见性,不会出现并发读的问题。返回的结果是factors对象,factors是局部变量,并未使cache对象逸出,所以这里也是线程安全的。
cache物件在service()中只有一處寫入操作(建立新的cache物件),其餘都是讀取操作,這裡符合volatile的應用場景,確保cache物件對其他執行緒的可見性,不會出現並發讀的問題。
回傳的結果是factors對象,factors是局部變量,並未使cache對象逸出,所以這裡也是線程安全的。
一下個人理解:
(1).cache物件在service()中只有一處寫操作,但是多個執行緒都會執行這個寫入操作。例如A執行緒帶入參數a1執行service方法之後,快取裡是number=a , lastFactors=[a];這時a2執行緒進入service方法帶入a,執行BigInteger[] factors = cache.getFactors(i); 取得了快取資料[a]進行判斷時執行緒切換,來了個C執行緒帶入參數c執行完了方法。實際上此時的快取是c和[c].理論上線程B讀的值已經是過期的了。 。 。只是因為「快取」的業務意義使得這個過期值不會造成程式錯誤罷了。 。 。也就是說這個例子的線程安全性體現在正好切合了這個業務。 。 。
(2).OneValueCache的不可變也是有限制的。 BigInteger[]類型的陣列接受外部參數後,用Arrays.copyOf能使得初始化後值不再改變。但是如果不是BigInteger類型而是其他類型,要保證該類型也是不可變物件才行。
總之我感覺,對於我這麼菜的初學者來說,鎖還是最好用的。畢竟程式的最佳化是建立在沒有BUG的基礎上。萬一哪邊因為理解偏差導致隱含漏洞就跪了。