在並發編程中,這種由於不恰當的執行時序而出現不正確的結果是一種非常嚴重的情況,它有一個正式的名字叫做:競態條件
使用“先檢查后執行”的一種常見情況就是延遲初始化。延遲初始化的目的是將對象的初始化操作推遲到實際被使用時才進行,同時要確保只被初始化一次。
@NotThreadSafe public class LazyInitRace { private ExpensiveObject instance = null; public ExpensiveObject getInstance() { if (instance == null) instance = new ExpensiveObject(); return instance; } } class ExpensiveObject { }
在上述代碼LazyInitRace 中包含了一個競態條件,它可能會破壞這個類的正確性。假定線程A和線程B 同時執行getInstance 方法。A 看到instance 為空,因此A創建一個新的ExpensiveObject實例。B 同樣需要判斷instance 是否為空。此時的instance是否為空,要取決於不可預測的時序,包括線程的調度方式,以及A 需要花多長時間來初始化ExpensiveObject並設置instance。如果當B檢查時,instance為空,那么在兩次調用 getInstance 時可能會得到不同的對象。
為了確保線程安全性,"先檢查后執行"(例如延遲初始化)和"讀取-修改-寫入" 等操作必須是原子的。
@ThreadSafe public class CountingFactorizer extends GenericServlet implements Servlet { private final AtomicLong count = new AtomicLong(0); public long getCount() { return count.get(); } public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); count.incrementAndGet(); encodeIntoResponse(resp, factors); } void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {} BigInteger extractFromRequest(ServletRequest req) {return null; } BigInteger[] factor(BigInteger i) { return null; } }
上述代碼是線程安全的,因為使用了AtomicLong類型確保數值和對象引用上的原子狀態轉換是安全的。因為這里的計數是安全的,所以這里的Servlet也是線程安全的。
@NotThreadSafe public class UnsafeCachingFactorizer extends GenericServlet implements Servlet { private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>(); private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>(); public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); if (i.equals(lastNumber.get())) encodeIntoResponse(resp, lastFactors.get()); else { BigInteger[] factors = factor(i); lastNumber.set(i); lastFactors.set(factors); encodeIntoResponse(resp, factors); } } void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) { } BigInteger extractFromRequest(ServletRequest req) { return new BigInteger("7"); } BigInteger[] factor(BigInteger i) { // Doesn't really factor return new BigInteger[]{i}; } }
上述代碼看似使用了AtomicReference,對於這些對象是原子性的,但是不能確保這兩個變量的組合是原子性的,也可能出現兩個對象存在數量不同的情況。
Java提供了一種內置的鎖機制來支持原子性:同步代碼塊(Synchronized Block)
synchronized 關鍵字,代表這個方法加鎖,相當於不管哪一個線程A每次運行到這個方法時,都要檢查有沒有其它正在用這個方法的線程B(或者C D等),有的話要等正在使用這個方法的線程B(或者C D)運行完這個方法后再運行此線程A,沒有的話,直接運行它包括兩種用法:synchronized 方法和 synchronized 塊。
@ThreadSafe public class SynchronizedFactorizer extends GenericServlet implements Servlet { @GuardedBy("this") private BigInteger lastNumber; @GuardedBy("this") private BigInteger[] lastFactors; public synchronized void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); if (i.equals(lastNumber)) encodeIntoResponse(resp, lastFactors); else { BigInteger[] factors = factor(i); lastNumber = i; lastFactors = factors; encodeIntoResponse(resp, factors); } } void encodeIntoResponse(ServletResponse resp, BigInteger[] factors) { } BigInteger extractFromRequest(ServletRequest req) { return new BigInteger("7"); } BigInteger[] factor(BigInteger i) { // Doesn't really factor return new BigInteger[] { i }; } }
這種同步機制使得要確保因數分解的Servlet的線程安全性變得更簡單。用了關鍵字synchronized來修飾service方法,因此在同一時刻只有一個線程可以執行service方法。現在的SynchronizedFactorizer是線程安全的。然而,這種方法卻過於極端,因為多個客戶端無法同時使用因數分解Servlet,服務的響應性非常低,無法令人接受。這是一個性能問題,而不是線程安全問題。
