天天看點

java并發實戰閱讀筆記 --1Java并發實戰閱讀筆記

Java并發實戰閱讀筆記

1.線程安全性

如果我們要編寫線程安全的代碼,核心就是要對狀态的通路操作進行管理,特别是對共享的和可變的狀态的通路,從非正式的意義上來說,對象的狀态是指對存儲在狀态變量中的資料。在java中,主要用來同步的關鍵字是synchronized,但”同步”這個術語還包括了volatile類型變量,顯示鎖以及原子變量。

volatile是一個輕量級的synchronized,與synchronized相比,volatile變量需要的編碼比較少。volatile變量具有synchronized的可見特性,但不具備原子特性。這就說明了線程能夠發現volatile變量的最新值。

我們看下面這個最簡單的例子:

public class UnsafeSequence {
    private int value;
    public int getNext() {
        return value++;
    }
}
           

這個getNext操作并不是原子性的,它必須先擷取value,然後修改,最後再寫入。在調用時序不同的時候,有可能會出現錯誤的狀況。

第一個線程: value = 9 ——–> value++ —————-> value =10

第二個線程: value = 9 ———–> value++ —————> value =10

上面這種情況,兩個線程分别調用了getNext,卻獲得了相同的值,這是一種錯誤的狀态。

當多個線程同時通路一個可變狀态變量時如果沒有使用适當的同步,就會出現錯誤,以下的方法可以避免這個錯誤:

  1. 不線上程之間共享該變量
  2. 将狀态變量改為不可變變量
  3. 通路狀态變量時使用同步操作。

是以,我們可以将上面的例子修改一下:

public class UnsafeSequence {
    private int value;
    public synchronized int getNext() {
        return value++;
    }
}
           

這樣,我們就給這個函數進行了同步,也就是不會出現上面提到的時序的錯誤了。

關于線程安全的定義,其最核心的概念就是正确性,所謂的正确性,就是某個類的行為與其規範完全一緻。

線程安全:當多個線程通路某個類時,這個類始終能保持正确的行為,那麼這個類就是線程安全的

無狀态對象一定時線程安全的,關于無狀态:既不包含任何域,也不包含任何對其他類域的引用。

下面是一個無狀态的例子:

public class StatelessFactorizer implements Servlet {
 public void Service(ServletRequest req, ServletResponse resp) {
   BigInter i = extractFromRequest(req);
   BigInteger[] factors = factor(i);
   encodeIntoResponse(resp,factors);
  }
 }
           

因為這個servlet不包含對其他類的引用,也不包含任何域,僅僅有幾個臨時變量用于存儲。是以這個在并發中并不會出現錯誤的狀态。可是一旦我們需要統計處理請求的數量,如果沒有利用同步機制,就會像下面的一樣:

public class StatelessFactorizer implements Servlet {
     private long count = 0;
     public void Service(ServletRequest req, ServletResponse resp) {
       BigInter i = extractFromRequest(req);
       BigInteger[] factors = factor(i);
       ++count;
       encodeIntoResponse(resp,factors);
      }
 }
           

這個就和最上面的那個例子是一樣的,++count同樣包含了3步操作,讀取-修改-寫入。

競态條件:由于不恰當的執行時序而出現的不正确的結果。當某個計算的正确性取決于多個線程的執行順序就會出現這種情況。競态條件最經常出現的情況有兩種,一種是先檢查後執行,一種是延遲初始化。

下面是一個先檢查後執行的例子:

public class CheckBeforeRun {
     private boolean run = true;
     public void runs() {//第一個線程執行到這裡,切換到第二個線程修改run的值。
         if(run) {
             doSomething();
         }
     }
 }
           

下面是一個延遲初始化的例子:

public class LazyInitRace {
  private ExpensiveObject instance = null;
  public ExpensiveObject getInstance() {
      if(instance == null) { //第一個線程執行到這裡,切換到第二個線程,instance仍為null
          instance = new ExpensiveObject();
          }
       return instance;
      }
 }
           

其實先檢查後執行和延遲初始化是差不多的,都是基于一個判斷去執行操作,而這個判斷的值又是線程共享的,一旦在這個時刻切換到另外一個線程,就有很大的幾率出錯。

如果要避免競态條件的出現,就必須在某個線程修改該變量的時候,通過某種方式防止其他線程使用這個變量,保證某個線程操作這個變量的操作是原子操作。如果要保持狀态的一緻性,就需要在單個原子操作中更新所有相關的狀态變量。

内置鎖:每個java對象都用可以用作一個實作同步的鎖,對象的内置鎖與其狀态之間沒有内在的關聯,僅僅隻是為了避免顯式去建立鎖對象的耗時操作,java中的内置鎖相當于是一個互斥鎖,這就意味着最多隻有一個線程能夠持有這種鎖。

java中的内置鎖是可重入的,意思就是能夠擷取該鎖的粒度是線程,而非調用。 java中利用一個引用計數值來标記一個線程持有該鎖的數量,如果線程請求一個未被持有的鎖時,這個鎖的引用計數值将會+1,如果同一個線程再次請求持有這個鎖,這個鎖的引用計數值将會再+1。直到該鎖的引用計數值為0,這個鎖才會被釋放。

下面是一個可重入的例子:

public class Widget { 
 public synchronized void doSomething(){...}
 }

public class LoggingWidget extends Widget {
 public Synchronized void doSomething() {
    ....
    super.doSomething();
  }
 }
           

子類函數加了一個同步鎖,父類函數也加了一個同步鎖,一旦鎖是不可重入的,子類函數調用了父類的函數,父類函數卻要等子類解鎖,這将會造成一個死鎖。

對于可能被多個線程同時通路的可變狀态變量,在通路它時都需要持有同一把鎖,每個共享和可變的變量都應該隻由一個鎖來保護,這樣才能便于區分。一種常見的加鎖約定是:将所有可變狀态都封裝在對象内部,并通過對象的内置鎖對所有通路可變狀态的代碼進行同步。

繼續閱讀