這篇記錄一下保證並發安全性的策略之——不變性。
(注意:是Immutable,不是Invariant!)
將一連串行為組織為一個原子操作以保證不變性條件,或者使用同步機制保證可見性,以防止讀到失效數據或者對象變為不一致狀態,這些問題都是因為共享了可變的數據。
如果我們能保證數據不可變,則這些復雜的問題就自然不用去考慮了。
不可變對象一定是線程安全的。
說簡單也簡單,不可變對象只有一種狀態,且由構造器控制。
因此,判斷不可變對象的狀態變得特別簡單。
當我們共享一個可變對象,其狀態的改變行為都是難以預料的,尤其是作為參數傳給了可覆蓋的方法時,更糟糕的是這些client代碼都可以保留該對象的引用,也就是說狀態改變的時機也同樣難以預料。
相對於可變對象的共享,不可變對象的共享則簡單很多,而且幾乎不用考慮弄一個快照。
於是我們現在有了一個新的問題:如何讓狀態不可變?
對於"不可變"這一說法無論是JLS還是什么地方都沒有明確的定義,但不可變絕對不僅僅是加個final修飾那么簡單,比如final修飾的field引用的是一個可變對象,而final保證的僅僅是引用的指向不會發生變化。
沒錯,不可變對象和不可變的對象引用是兩碼事。
對於如何構建一個不可變對象,我們有三個條件(雖然說是"條件",但並不是那么硬性的,可以算是某種建議):
-
對象創建后保證狀態不可變
-
對象的所有field都是final
- 創建期間沒有逸出自身引用,保證對象的創建正確。
關於上面三條,這里舉一個例子:
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);
}
public String getStoogeNames() {
List<String> stooges = new Vector<String>();
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
return stooges.toString();
}
}
讓我們檢查一下是否滿足三個條件:
-
對象創建后保證狀態不可變,是否有變化? 我們首先是用private修飾了stooges,接着提供的兩個公有方法中第一個方法是返回boolean而第二個方法getStoogeNames中我們重新創建了一個stooges且保證相同的邏輯而不是直接引用stooges field。
-
對象的所有field都是final,很明顯,我們用了final進行描述以防止對象狀態在對象生命周期內改變其引用。
-
創建期間沒有逸出自身引用,在stooges聲明時我們就指定了引用,並在構造函數中將其初始化,不會有外來方法可以引用到該狀態並將其改編。
不得不說這個final修飾是關鍵。
通常我們對final關鍵字最直觀的印象是,如果一個用final修飾的對象引用的指向是不會改變的(發現這話怎么說都很難表達清楚,但是你懂的),但即使引用了可變的實例,就判斷狀態而言,加了final就可以簡化不少,分析基本不可變的對象總比分析完全可變的對象來得容易多了吧....
而final和synchronized關鍵字那樣也有多個語義,就是——能確保初始化過程的安全性,從而可以自由共享,不需要進行同步處理(這個同步處理不包括可見性)。
下面是一段用final(更確切地說應該是不可變性)保證了操作原子性(以保證可變性條件)一段例子。
某個Servlet接收參數后將參數傳入factor方法對其進行運算並將結果進行響應。
假設這個factor方法非常耗時,於是我們想出了一個方法暫時緩解這一狀況,即下一次請求的參數和上一次請求的參數相同則響應緩存中的結果。
也就是說每一次請求時我們多了一個步驟,也就是需要判斷請求的數字是否和緩存中的一樣,如果不同則重新計算,而這一段並不是原子操作,並發出現時會出現破壞可變性條件的情況。
而為了應對這個問題,我們可以將這一部分用synchronized保證其原子性,但這里使用另一種方式,使用不可變對象:
public 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);
}
}
這個不可變對象是如何設計的?
首先我們保證了所有狀態用final進行修飾並在唯一的構造器中進行初始化,注意構造器中對lastFactors進行初始化的那一段,我們用Arrays.copyOf保證了其正確構造,也就是防止逸出。
然后是唯一一個公有方法,這個方法要返回的正是我們計算好的factors,但我們不能直接返回factors,也是為了防止逸出,我們使用了Arrays.copyOf。
下面是使用緩存的Servlet,整個對象只有一個field就是cache,我們用volatile修飾以保證並發時的可見性,即線程A改變了引用時線程B可以立即看到新的緩存。
public class VolatileCachedFactorizer extends GenericServlet 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) {
factors = factor(i);
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(resp, factors);
}
void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
return new BigInteger[]{i};
}
}