【Java併發.2】線程安全性

  要編寫線程安全的代碼,其核心在於要對狀態訪問操做進行管理,特別是對共享(Shared)和可變的(Mutable)狀態的訪問。java

  「共享」意味着變量能夠由多個線程同時訪問,而「可變」則意味着變量的值在其生命週期內能夠發生變化。咱們將像討論代碼那樣來討論線程安全性,但更側重於如何防止數據在數據上發生不可控的併發訪問。緩存

  當多個線程訪問某個狀態變量而且其中有一個線程執行寫入操做時,必須採用同步機制來協同這些線程對變量的訪問。Java 中的主要同步機制是關鍵字 synchronized ,它提供了一種獨佔的加鎖方式,但「同步」這個術語還包括 volatile 類型的變量,顯式鎖(Explicit Lock)以及原子變量。安全

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

  若是從一開始就設計一個線程安全的類,那麼比在之後再將這個類修改成線程安全的類要容易的多。網絡

當設計線程安全的類時,良好的面向對象技術、不可修改性,以及明晰的不變性規範都能起到必定的幫助做用。

 

2.1  什麼是線程安全性  併發

   給線程安全性給出一個確切的定義:less

當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些線程將如何交替執行,而且在主調代碼中不須要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱爲這個類時線程安全的。 
在線程安全類中封裝了必要的同步機制,所以客戶端無須進一步採起同步措施。

  示例:一個無狀態的 Servletide

public class StatelessFactorizer implements Servlet {
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        BigInteger i = extractFromRequest(servletRequest);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(servletResponse, factors)
    }
}

  與大多數Servlet 相同,StatelessFactorizer 是無狀態的:它既不包含任何域,也不包含任何對其餘類中域的引用。計算過程當中的臨時狀態僅存在於線程棧上的局部變量中,而且只能由正在執行的線程訪問。性能

無狀態對象必定是線程安全的。

  

2.2  原子性this

  假設咱們但願增長一個「命中計數器」來統計所處理的請求數量。例以下代碼atom

public class StatelessFactorizer implements Servlet {        【皺眉臉--不要這麼作private long count = 0;
    public long getCount() {
        return count;
    }
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        BigInteger i = extractFromRequest(servletRequest);
        BigInteger[] factors = factor(i);
        ++count;
        encodeIntoResponse(servletResponse, factors)
    }
}

  不幸的是 ++count 不是原子操做,這是一個「讀取-修改-寫入」的操做序列。(上一節講的競態條件)。

  2.2.1  競態條件

    當某個計算的正確性取決於多個線程的交替執行時序時,那麼就會發生競態條件。

    最多見的競態條件類型就是「先檢查後執行(Check-Then-Act)」操做,即經過一個可能失效的觀測結果來決定下一步的動做。

  2.2.2  實例:延遲初始化中的競態條件

public class LazyInitRace {            【皺眉臉--不要這樣作private UnsafeSequence instance = null;
    public UnsafeSequence getInstance() {
        if (instance == null) {
            instance = new UnsafeSequence();
        }
        return instance;
    }
}

    存在另外一種競態條件,在「讀取-修改-寫入」這種操做。

  2.2.3  複合操做

    LazyInitRace 須要以原子方式執行(不可分割)的操做。要避免競態條件問題,就必須在某個線程修改該變量時,經過某種方式防止其餘線程使用這個變量,從而確保其餘線程只能在修改操做完成以前或以後讀取和修改狀態,而不是在修改狀態的過程當中。

假定有兩個操做A 和B ,若是從執行 A 的線程來看,當另外一個線程執行 B 時,要麼將 B 所有執行完,要麼徹底不執行 B,那麼 A 和 B 對彼此來講就是原子的。原子操做是指:對於訪問同一個狀態的全部操做(包括該操做自己)來講,這個操做時一個以原子方式執行的操做。

    因此,咱們將介紹加鎖機制,這是Java 中用於確保原子性的內置機制。使用 AtomicLong 類型的變量來統計已處理請求的數量。

public class StatelessFactorizer implements Servlet {
    private AtomicLong count = new AtomicLong(0);
    public long getCount() {
        return count.get();
    }
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        BigInteger i = extractFromRequest(servletRequest);
        BigInteger[] factors = factor(i);
        count.incrementAndGet();
        encodeIntoResponse(servletResponse, factors);
    }
}

    在 java.util.concurrent.atomic包中包含了一些原子變量類,用於實如今數值和對象引用上的原子狀態轉換。

 

2.3  加鎖機制

  當在Servlet 中添加一個狀態變量時,能夠經過線程安全的對象來管理 Servlet 的狀態以維護 Servlet 的線程安全性。但若是想在 Servlet 中添加更多的狀態,那麼是否只須要添加更多的線程安全狀態變量就足夠了?

  假設咱們但願提高 Servlet 的性能:將最近的計算結果緩存起來。看代碼:

public class UnsafeCachingFactorizer implements Servlet {                    【皺眉臉--不要這樣作private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
    private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        BigInteger i = extractFromRequest(servletRequest);
        if (i.equals(lastNumber.get())) {
            encodeIntoResponse(servletResponse, lastFactors.get());
        } else {
            BigInteger[] factors = factor(i);
            lastNumber.set(i);      //同步1
            lastFactors.set(factors);  //同步2
            encodeIntoResponse(servletResponse, factors);
        }
    }
}

  儘管這裏面的原子引用自己都是線程安全的,可是 仍是存在着競態條件。同步1同步2 不是原子操做,可能存在問題。

要保持狀態的一致性,就須要在單個原子操做中更新全部相關的狀態變量

  2.3.1  內置鎖

    Java提供了一種內置的鎖機制來支持原子性:同步代碼塊(Synchronized Block)。同步代碼塊包括兩部分:一個做爲鎖的對象引用,一個做爲由這個鎖保護的代碼塊。靜態的synchronized 方法以 Class 對象做爲鎖。

    Java 的內置鎖至關於一種互斥鎖,這意味着最多隻有一個線程能持有這種鎖。由這個鎖保護的同步代碼會以原子方式執行,多個線程在執行該代碼塊時也不會相互干擾。

    在上一個程序清單裏,若是使用關鍵字 synchronized 來修飾 service 方法,所以在同一個時刻只有一個線程能夠執行 service 方法。然而,這種方法卻過於極端,由於多個客戶端沒法同時使用Servlet ,服務響應性很是低,沒法使人接受。

  2.3.2  重入

    當某個線程請求一個由其餘線程持有的鎖時,發出請求的線程會阻塞。然而,因爲內置鎖是可重入的,所以若是某個線程試圖得到一個由本身持有的鎖,那麼這個請求就會成功。

    「重入」 意味着獲取鎖的操做的粒度是「線程」,而不是「調用」。

    下面代碼清單:若是內置鎖不是可重入的,那麼這段代碼將發生死鎖。

public class Widget {
    public synchronized void doSomething() {
      ...  
    }
}
public class LoggingWidget extends Widget {
     public synchronized void doSomething() {
        System.out.println(toString() + ": calling doSomething");
        super.doSomething();
    }   
}

 

2.4  用鎖來保護狀態

  因爲鎖能使其保護的代碼路徑以串行形式來訪問,所以能夠經過鎖來構造一些協議來實現對共享狀態的獨佔訪問。

對於可能被多個線程同時訪問的可變狀態變量,在訪問它時都須要持有同一個鎖,在這種狀況下,咱們稱狀態變量是由這個鎖保護的。
每一個共享的和可變的變量都應該只由一個鎖來保護,從而使維護人員指導是哪個鎖。

  一種常見的加鎖約定是,將全部的可變狀態都封裝在對象內部,並經過對象的內置鎖對全部訪問可變狀態的代碼路徑進行同步,使得在該對象上不會發生併發訪問。 

  當類的不可變性條件涉及多個狀態變量時,那麼還有另一個需求:在不變性條件中的每一個變量都必須由同一個鎖來保護

  若是同步能夠避免競態條件,那麼爲何不在每一個方法聲明時都使用同步呢?事實上,若是不加區別的濫用 synchronized ,可能致使程序中出現過多的同步。

  此外,將每一個方法都做爲同步方法還可能致使活躍性問題(Liveness)或性能問題(Performance)。

 

2.5  活躍性與性能

  以前有介紹對整個 service 方法進行同步,雖然這種簡單且粗粒度的方法能確保線程安全性,但付出的代價卻很高。咱們將這種應用程序稱爲不良併發(Poor Concurrent)應用程序:可同時調用的數量,不只受到可用處理資源的限制,還受到應用程序自己結構的限制。 

  緩存最近執行因數分解的數值 及其計算結果的 Servlet:

public class UnsafeCachingFactorizer 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;
    }
    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        BigInteger i = extractFromRequest(servletRequest);
        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(servletResponse, factors);
    }
} 

   在上面的改造後的代碼 實現了在簡單性(對整個方法進行同步) 與併發性(對儘量短的代碼路徑進行同步)之間的平衡。在獲取與釋放鎖等操做上都須要必定的開銷,所以若是將同步代碼塊分解的過細,那麼一般並很差,儘管這樣不會破壞原子性。

一般,在簡單性與性能之間存在着相互制約因素。當實現某個同步策略時,必定不要盲目地爲了性能而犧牲簡單性(着可能會破壞安全性)

  不管是執行計算密度的操做,仍是在執行某個可能阻塞的操做,若是持有鎖的時間過長,那麼都會帶來活躍性或性能問題  

當執行時間較長的計算或者可能沒法快速完成的操做時(例如:網絡 I/O 或控制檯 I/O),必定不要持有鎖。
相關文章
相關標籤/搜索