Java併發編程學習筆記(一)線程安全性 1

什麼是線程安全性:

   要編寫線程安全的代碼,其核心在於要對狀態訪問操作進行管理,特別是對共享的和可變的狀態的訪問。“共享”意味着變量可以由多個線程同時訪問,而“可變”則意味着變量的值在其生命週期內可以發生變化。

    原文出處:http://liuxp0827.blog.51cto.com/5013343/1412874

   一個對象是否需要線程安全的,取決於他是否被多個線程訪問。這指的是在程序中訪問對象的方式,而不是對象要實現的功能。要使得對象時線程安全的,需要採用同步機制來協同對對象可變狀態的訪問。如果無法實現協同,那麼可能導致數據破壞以及其他不該出現的結果。

如果當多個線程訪問同一個可變的狀態變量時沒有使用合適的同步,那麼程序就會出現錯誤。有三種方式可以修復這個問題:1.不在線程之間共享該狀態變量;2.將狀態變量修改爲不可變的變量;3.在訪問狀態變量時使用同步。

   在線程安全性的定義中,最核心的概念就是正確性。正確性的含義是,某個類的行爲與其規範完全一致。當多個線程訪問某個類時,不管運行時環境採用何種調度方式或者這些線程將如何交替執行,並且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼稱這個類是線程安全的。

   來看一個基於Servlet的因數分解服務,並逐漸擴展它的功能,同時確保它的線程安全性。{}

//原文出處:http://liuxp0827.blog.51cto.com/5013343/1412874
@ThreadSafe
public class StatelessFactorizer implements Servlet {
    public void service (ServletRequest req,ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(resp,factors);
    }
}


   與大多數Servlet一樣,StatelessFactorizer是無狀態的,它不包含任何域,也不包含任何對其他類中域的引用。計算過程中的臨時狀態僅存在於線程棧上的局部變量中,並只能有正在執行的線程訪問。因此無狀態對象一定是線程安全的


原子性


   假設我們希望增加一個“命中計數器”來統計所處理得請求數量。一種直觀的方法是在Servlet中增加一個long類型的域,並且每處理一個請求就將這個值加1:

//原文出處:http://liuxp0827.blog.51cto.com/5013343/1412874
@NotThreadSafe                  //不好的代碼
public class UnsafeCountingFactorizer implements Servlet {
    private long counts = 0;                                                           
    public long getCounts(){return counts;}  
                        
    public void service (ServletRequest req,ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        ++counts;
        encodeIntoResponse(resp,factors);
    }
}

UnsafeCountingFactorizer是非線程安全的。雖然遞增造作++counts是一種緊湊的語法,使其看上去只是一個操作,但這個操作並非原子的,因而他並不會作爲一個不可分割的操作來執行。它包含了三個獨立的操作“讀取counts ——> 修改counts+1 ——> 寫入counts”。 假設計數器的初始值爲9,那麼在某些情況下,每個線程督導的值都是9,接着執行遞增操作,並且都將計數器的值設爲10,那麼命中計數器的值將會偏差1.這種由不恰當的執行時序而出現的不正確的結果是一種競態條件。

   當某個計算的正確性取決於多個線程的交替執行時序時,那麼就會發生競態條件。最常見的競態條件類型就是“先檢查後執行”操作:通過一個可能失效的觀測結果決定下一步的動作。

   “先檢查後執行”的一種常見情況就是延遲初始化,即將對象的初始化操作推遲到實際被使用時才進行,同時要確保只備初始化一次。

//原文出處:http://liuxp0827.blog.51cto.com/5013343/1412874
@NotThreadSafe
public class LazyInitRace {
    private ExpensiveObject instance = null;
    public ExpensiveObject getInstance() {
        if(instance == null)
            instance = new ExpensiveObject();
        return instance;
    } 
}


   在LazyInitRace中包含了一個競態條件,它可能會破壞這個類的正確性。

LazyInitRaceUnsafeCountingFactorizer都包含一組需要以原子方式執行的操作。要避免競態條件問題,就必須在某個線程修改該變量時,通過某種方式防止其他線程使用這個變量,從而確保其他線程只能在修改操作完成之前或之後讀取和修改狀態,而不是在修改狀態的過程中。

   如果UnsafeCountingFactorizer的遞增操作是原子操作,那麼競態條件就不會發生。爲了確保線程安全性,“先檢查後執行”和“讀取-修改-寫入”等操作必須是原子的。我們把“先檢查後執行”和“讀取-修改-寫入”等操作統稱爲複合操作:包含了一組必須以原子方式執行的操作以確保線程安全性。

//原文出處:http://liuxp0827.blog.51cto.com/5013343/1412874
@ThreadSafe            
public class CountingFactorizer implements Servlet {
    private final AtomicLong counts = new AtomicLong(0);
    public long getCounts(){return counts.get();}
    
    public void service (ServletRequest req,ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        counts.incrementAndGet();
        encodeIntoResponse(resp,factors);
    }
}


   在實際情況下,應儘可能地使用現有的線程安全對象來管理類的狀態。與非線程安全的對象相比,判斷線程安全對象的可能狀態及其狀態轉換情況要更爲容易,從而也更容易維護和驗證線程安全性。


加鎖機制

   當在Servlet中添加狀態變量時,可以通過線程安全的對象來管理Servlet的狀態以維護Servlet的線程安全性。但如果想在Servlet中添加更多的狀態,只添加更多線程安全狀態變量是不夠的。我們希望提升Servlet的性能:將最近的計算結果緩存起來,dangliangge 連續的請求對相同的數值進行因數分解時,可以直接使用上一次的結果,而無需重新計算。這時,需要保存兩個狀態:最近執行因數分解的數值,以及分解結果。前面通過AtomicLong以線程安全的方式來管理計數器狀態,是否可以使用類似的AtomicReference來管理最近執行因數分解的數值以及分解結果?

//原文出處:http://liuxp0827.blog.51cto.com/5013343/1412874
@NotThreadSafe          
public class UnsafeFactorizer 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,lastNumber.get());
        else{
            BigInteger[] factors = factor(i);
            lastNumber.set(i);
            lastFactors.set(factors);
            encodeIntoResponse(resp,factors);
        }
    }


   儘管這些原子引用本身都是線程安全的,但在UnsafeCachingFactorizer中存在着競態條件,這可能產生錯誤的結果。

   在線程安全性的定義中要求,多個線程之間的操作無論採用何種執行時序或交替方式,都要保證不變性不被破壞。UnsafeCachingFactorizer的不變性條件之一是:在lastFactors中的緩存的因數之積應該等於在lastNumber中的緩存值。當在不變性條件中涉及多個變量時,各個變量之間並不是彼此獨立的,而是某個變量的值會對其他變量的值產生約束。因此,當更新某一個變量時,需要在同一個原子操作中對其他變量同時進行更新。

   在上述代碼中,儘管set方法的每次調用都是原子的,但仍然無法同時更新lastNumber和lastFactors。如果只修改了其中一個變量,那麼在這兩次修改操作之間,其他線程將發現不變性條件被破壞了。

   所以,要保持狀態的一致性,就需要在單原子操作中更新所有相關的狀態變量。

   未完待續... Java併發編程學習筆記(二)線程安全性 2    



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章