標籤:封裝 void 拋出異常 可見度 less 同步 存在 實現 一個
更新時間:2017-06-03
《Java並發編程實戰》文摘,有興趣的朋友可以買本紙質書仔細研究下。
一 執行緒安全性1.1 什麼是執行緒安全性
當多個線程訪問某個類時,不管運行時環境採用何種調度方式或者這些線程如何交替執行,並且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那麼就稱這個類是安全執行緒的。
線上程安全類中封裝了必要的同步機制,因此用戶端無須進一步採取同步措施。
樣本:一個無狀態的Servlet
/** * @author Brian Goetz and Tim Peierls */@ThreadSafepublic 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 }; }}
與大多數Servlet相同,這個類是無狀態的:它既不包含任何域,也不包含任何對其他類中域的引用。計算過程中的臨時狀態僅存於線程棧上的局部變數中,並且只能由正在執行的進程訪問。由於線程訪問無狀態對象的行為並不會影響其他線程中操作的正確性,因此無狀態對象是安全執行緒的。
1.2 原子性
一個線程不安全的樣本
@NotThreadSafepublic 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 }; }}
count域(執行個體變數)可以在多個線程中共用,++count又不是一個原子操作,它包含了讀取-修改-寫入等操作。當兩個線程在沒有同步的情況下同時對count進行操作將會導致不正確的結果,這兩個線程很有可能讀到相同的值並且同時都執行了遞增操作。
這種由於不恰當的執行時序而出現不正確的結果是一種非常重要的情況,它有一個正式的名字:競態條件。
要避免競態條件問題,就必須在某個線程修改該變數時,通過某種方式防止其他線程使用這個變數,從而確保其他線程只能在修改操作完成之前或之後讀取和修改狀態,而不是在修改狀態的過程中。每個線程執行的都是原子操作,要麼這個線程完全執行完,要麼完全不執行。
下面我們修改之前的UnsafeCountingFactorizer類,使它成為一個安全執行緒的類:
@ThreadSafepublic 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; }}
在java.util.concurrent.atomic包中有一些原子變數類,用於實現在數值和對象引用上的原子狀態轉換。通過使用AtomicLong來替代long類型的計數器,能夠確保所有對計數器狀態的訪問操作都是原子的。
在實際情況中,應儘可能地使用現有的安全執行緒對象來管理類的狀態。與非安全執行緒的對象相比,判斷安全執行緒對象的可能狀態及其狀態轉換情況要更為容易,從而也更容易維護和驗證執行緒安全性。
1.2 加鎖機制
當在Servlet中添加一個狀態變數時,可以通過安全執行緒的對象來管理。如果想添加更多的狀態,那麼是否只需添加更多的安全執行緒狀態變數就足夠了?
答案是否定的,要想保持狀態一致性,就需要在單個原子操作中更新所有相關的狀態變數。
Java提供了一種內建的鎖機制來支援原子性:同步代碼塊。它包括兩部分:一個作為鎖的對象引用,一個作為由這個鎖保護的代碼塊。以關鍵字synchronized來修飾的方法就是一種橫跨整個方法體的同步代碼塊,其中該同步代碼塊的鎖就是方法調用所在的對象。靜態synchronized方法以Class對象作為鎖。
synchronized (lock) { // 訪問或修改由鎖保護的共用狀態}
每個Java對象都可以作為一個用來實現同步的鎖,這些鎖被稱為內建鎖或監視器鎖。線程在進入同步代碼塊之前會自動獲得鎖,並且在退出同步代碼塊時自動釋放鎖,無論是正常退出還是通過從代碼塊中拋出異常退出。獲得內建鎖的唯一途徑就是進入由這個鎖保護的同步代碼塊或方法。
Java的內建鎖相當於一種互斥鎖,這意味著最多隻能有一個線程持有這種鎖。如果某個線程持有這種鎖由於某種原因沒有釋放鎖,那麼別的線程將永遠等待下去。雖然這個鎖保護的同步代碼塊會以原子方式執行,保證了安全執行緒,然而這種方法過於極端,因為多個用戶端無法同時使用這個Servlet,服務的響應性非常低,無法令人接受。
幸運的是,通過縮小同步代碼塊的範圍,我們很容易做到既確保Servlet的並發行,同時又維護線程的安全性。要確保同步代碼塊不要過小,並且不要將本應是原子的操作拆分到多個同步代碼塊。應該盡量將不影響共用狀態且執行時間較長的操作從同步代碼塊中分離出去,從而在這些操作的執行過程中,其他線程可以訪問共用狀態。
當執行時間較長的計算或者可能無法快速完成的操作時(例如:網路I/O)一定不要持有鎖。
二 對象的共用2.1 非原子的64位操作
當線程在沒有同步的情況下讀取變數時,可能會得到一個實效值,但至少這個值是由之前某個線程設定的值,而不是一個隨機值。這種安全保證被稱為最低安全性。
最低安全性適用於絕大多數變數,但是存在一個例外:非volatile類型的64位元值變數(double和long)。JVM允許將64位的讀寫操作分解為兩個32位的操作。如果對某個非volatile類型的long變數的讀和寫操作在不同的線程中執行,那麼很可能讀到某個值的高32位和另一個值的低32位。所以在多線程程式中使用共用且可變的64位元值變數是不安全的,最低安全性也無法保證。除非我們使用關鍵字volatile來聲明它們,或者使用鎖保護起來。
2.2 加鎖與可見度
加鎖的含義不僅僅局限與互斥行為,還包括記憶體可見度。為了確保所有線程都能看到共用變數的最新值,所有執行讀寫操作的線程都必須在同一個鎖上同步。
2.3 Volatile變數
Java語言提供了一種稍弱的同步機制,即volatile變數,用來確保將變數的更新操作通知到其他線程。當把變數聲明為volatile類型後,編譯器與運行時都會注意到這個變數是共用的,因此不會將該變數上的操作與其他記憶體操作一起重排序。volatile變數不會被緩衝在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變數時總是會返回最新寫入的值。
僅當volatile變數能簡化代碼的實現以及對同步策略的驗證時才應該使用它們。如果在驗證正確性時需要對可見度進行複雜的判斷,那麼就不要使用volatile變數。volatile變數的正確使用方式包括:
- 確保它們自身的可見度。
- 確保它們所引用對象的狀態的可見度
- 標識一些重要的程式生命週期事件的發生(例如,初始化或者關閉)
加鎖機制既可以確保可見度又可以確保原子性,而volatile變數只能確保可見度。
未完,待續...
《Java並發編程實戰》文摘