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

什麼是線程安全性:java

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

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

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

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

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

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

//原文出處: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是無狀態的,它不包含任何域,也不包含任何對其餘類中域的引用。計算過程當中的臨時狀態僅存在於線程棧上的局部變量中,並只能有正在執行的線程訪問。所以無狀態對象必定是線程安全的ide


原子性性能


   假設咱們但願增長一個「命中計數器」來統計所處理得請求數量。一種直觀的方法是在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    

相關文章
相關標籤/搜索