線程安全定義:
當多個線程訪問一個類的時候,如果不用考慮這些線程在運行時環境下的調度和交替執行,並且不需要額外的同步及在調用方法代碼不必做其他的協調,這個類的行爲任然是正確的,那麼稱這個類是線程安全的。
所有的例子都是一個servlet用來進行因數分解,通過request傳入一個數字,然後service方法調用factor方法進行因數分解。
1. 無狀態的對象永遠線程安全
public class StatelessFactorizer extends GenericServlet implements Servlet {
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
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 };
}
}
這個類沒有一個私有或公共的變量,是一個無狀態的類。因爲多個線程進入service方法,所有的service方法內的局部變量都封裝在各個線程的棧中,只有本線程可以訪問,所以,這個類是線程安全的。
2. 原子性
@NotThreadSafe
public class UnsafeCountingFactorizer extends GenericServlet implements Servlet {
private long count = 0;
public long getCount() {
return count;
}
public void service(ServletRequest req, ServletResponse resp) {
BigInteger i = extractFromRequest(req);
BigInteger[] factors = factor(i);
++count;
encodeIntoResponse(resp, factors);
}
void encodeIntoResponse(ServletResponse res, BigInteger[] factors) {
}
BigInteger extractFromRequest(ServletRequest req) {
return new BigInteger("7");
}
BigInteger[] factor(BigInteger i) {
// Doesn't really factor
return new BigInteger[] { i };
}
}
我們在第一個例子上,添加了一個功能計數。每調一次service方法,count加1。很可惜,這個類不是線程安全的,因爲++count不是一個原子操作,這是一個讀-改-寫三個步驟的操作。這樣我們就很容易理解這個類爲什麼不是線程安全的了,線程A讀到count值爲1,然後修改爲2。在線程A將count的值寫回去之前,線程B讀取了count的值。結果是線程A將count的值改爲2,線程B也將count的值改爲2。至於什麼是原子操作,我也沒看懂,定義如下:
假設有操作A和B,如果從執行A的線程的角度看,當其他線程執行B的時候,要麼B全部執行完成,要麼一點也沒有執行,這樣A和B互爲原子操作。一個原子操作是指:該操作對於所有的操作,包括它自己,都滿足前面描述的狀態。(我已經暈了)
讓count加一操作變成原子操作的代碼如下:
@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是什麼鬼,這個大家去問度娘吧。簡單說,它在讀-改-寫的第三步寫操作時還會回去查看值是否已經被改變,如果被改變,重新加載。(可以百度搜索java cas)
3. 鎖
我們上面所說的自增操作,通過atomic類變成了原子操作,因此類也就變成線程安全的了。但是如果有多個全局變量,並且這多個全局變量的值具有關聯性。場景如下:我們想實現應對連續兩個客戶請求相同的數字進行因數分解,於是我們緩存了最新的計算結果
@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 };
}
這個類不是線程安全的,因爲lastNumber和lastFactors這兩個變量是有關係的,不是獨立的。我們的atomic只能保證他們自身的操作是線程安全,無法保證兩個一起是線程安全的。假設,lastNumber被線程A修改爲了10,而lastFactors還是舊值9的因數分解結果,那線程B剛好請求數字10的因數分解結果,就拿到了9的因數分解結果。
這裏我們很容易想到用synchronized關鍵字來給service方法加鎖,我就不貼代碼了,這樣做的確能夠解決問題,但是service方法一次只能進入一個線程了,性能存在很大問題。我們在做併發編程的時候,一定要兼顧性能,最好的方式如下所示:
@ThreadSafe
public class CachedFactorizer extends GenericServlet implements Servlet {
@GuardedBy("this") private BigInteger lastNumber;
@GuardedBy("this") private BigInteger[] lastFactors;
@GuardedBy("this") private long hits;
@GuardedBy("this") 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);
}
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};
}
}
把鎖打碎在方法裏面,第一個synchronized塊保護着檢查再運行的操作,另一個保證緩存的number和factor同步更新。提醒一點,在加鎖期間,不要進行耗時操作,我們把鎖移到方法裏面,主要目的就是將那些耗時操作剔除到同步塊之外。