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