在前篇我們透過 Atomic Variable 成功替 Servlet 加了一個 state,但如果我有更多的 state 要維護,是不是就是多加一些 Atomic state 就好了?
假設有一個情境,我們要在 Servlet 針對某個計算結果作 caching,以便在另一個 request 進來的時候不用重新計算,我們可能會如以下實作:
public class UnsafeCachingFactorizer 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);
}
}
}
如果有多個相關的 state,要如何保持它們的 Thread safety 呢?
一種方法是透過 Java 的 Intrinsic lock,也就是 synchronized 這個保留字所建立的區塊,而建立這個區塊有兩個部份
在 Java 中的 Intrinsic lock 是一種 mutex,換句話說同時最多只有一個執行緒可以持有 lock
因此,上面的 Servlet 我們可以調整如下:
public class SynchronizedFactorizer implements Servlet {
private BigInteger lastNumber;
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);
}
}
}
Instrinsic lock 還有一個特性就是 Reentrancy
只要加了 syncrhonized 就一定執行緒安全了嗎?
一個常見的作法就是把所有 mutable 的 state 都放到一個類別,並處理並行的存取,以確保這些 state 都是執行緒安全的
並非所有的 state 都要處理同步,只有 mutable 的 state 需要
不要無腦將整個方法都用 synchronized 包住
如果有耗時的操作如網路連線、檔案I/O等…盡可能將它們排除在外
不這麼做的話,很容易在大流量的情況下卡住所有的 request
之前的 SynchronizedFactorizer 可以調整如下:
public class SynchronizedFactorizer implements Servlet {
private BigInteger lastNumber;
private BigInteger[] lastFactors;
private long hits;
private long cacheHits;
public synchronized long getHits() {
return hits;
}
public synchronized double getCacheHitRatio() {
return (double) cacheHits / (double) hits;
}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = null;
synchronized (this) {
++hits;
if (i.equals(lastNumber)) {
++cacheHits;
factors = lastFactors.clone();
}
}
if (factors == null) {
factors = factor(i);
synchronized (this) {
lastNumber = i;
lastFactors = factors.clone();
}
}
encodeIntoResponse(resp, factors);
}
}