1. 程式人生 > >高併發下的執行緒安全實現——互斥同步

高併發下的執行緒安全實現——互斥同步

好久沒來csdn上寫部落格了,去年(16年)來到杭州後,忙得沉澱的時間都沒有了,這段時間空閒下來,慢慢補上!
執行緒允許多個活動同時進行,併發下有很多東西可能出錯,比如資料錯誤,程式執行異常。很多時候這些錯誤以及異常在測試中都很難重現,他們可能是間歇性的,且與時間相關,程式的行為在不同的VM上可能表現根本不同。所以設計一個執行緒安全的程式就顯得尤為重要,尤其是在高併發環境下。
執行緒安全實現的方法主要有:互斥同步、非阻塞同步(CAS)、執行緒區域性變數(threadLocal)、wait和notify、java.util.concurrent併發工具包、volatile保證變數的執行緒安全等。由於CAS存在ABA問題,以及wait和notify在java1.5版本後,java平臺就提供了java.util.concurrent來完成以前必須在wait和notify上手寫程式碼來完成的各項工作,所以執行緒安全這一系列文章我們主要討論互斥同步、執行緒區域性變數、java.util.concurrent以及volatile這幾種執行緒安全實現的方法。接下來每一篇文章討論一種實現方法,對於沒有討論的方法讀者可以自己去研究。不過這裡還是要提醒一下,如果一定要使用wait和notify,那麼應該始終使用wait迴圈模式來呼叫wait方法,使用notifyAll來代替notify,具體原因讀者可參考effective java,本文對此不做深入討論。
在java中,一般使用synchronized關鍵字來達到互斥同步的效果。Synchronized關鍵字經過編譯後,會在同步塊的前後分別形成monitorenter和monitorexit這兩個位元組碼指令,這兩個位元組碼都需要一個reference型別的引數來指明要鎖定和解鎖的物件。如果java程式中synchronized明確指定了物件引數,那就是這個物件的reference;如果沒有明確指定,那就根據synchronized修飾的是例項方法還是類方法,去取對應的物件例項或類物件來作為鎖物件。雖然synchronized可以保證在同一時刻,只有同一個執行緒可以訪問某一方法或者程式碼塊,但是鎖機制每次阻塞或喚醒一個執行緒的時候,都需要作業系統來完成,這裡就涉及到系統狀態轉換的問題(從使用者態轉換到核心態),這個過程會耗費CPU很多的時間。所以使用加鎖同步機制來處理多執行緒安全的問題,這個代價還是比較大的,需要程式慎密地分析什麼時候對變數進行讀寫,什麼時候需要鎖定某個物件,什麼時候釋放物件鎖等繁雜的問題。
我們來分析一下懶漢單例模式的多執行緒安全:

public class LiuManSingleton {

  private static LiuManSingleton instance = null;

  private LiuManSingleton(){}

  public static LiuManSingleton getInstance() {
      if(instance == null){//懶漢式
          instance = new LiuManSingleton();
      }
      return instance;
  }
}

這裡實現了懶漢單例模式,但是這個實現並不是執行緒安全的,我們編寫測試用例來測試一下:

@Test
public void test01(){
    ExecutorService service = Executors.newCachedThreadPool(); // 建立一個執行緒池
    for (int i = 0; i < 10; i++) {
      Runnable runnable = new Runnable() {
        public void run() {
          try {
            System.out.println(LiuManSingleton.getInstance().hashCode());

          } catch
(Exception e) { e.printStackTrace(); } } }; service.execute(runnable);// 為執行緒池新增任務 } }

執行結果如下:
538975307
88071440
88071440
88071440
88071440
88071440
88071440
88071440
88071440
88071440
可以看到,這個單例模式是執行緒非安全的,出現這個問題,是由於多個執行緒可以同時進入getInstance()方法,那麼只需要對該方法進行synchronized的鎖同步即可:

public class LiuManSingleton {

  private static LiuManSingleton instance = null;

  private LiuManSingleton(){}

  public synchronized static LiuManSingleton getInstance() {
      if(instance == null){//懶漢式
          instance = new LiuManSingleton();
      }
      return instance;
  }
}

再次呼叫上面的測試用例,執行結果如下:
396953361
396953361
396953361
396953361
396953361
396953361
396953361
396953361
396953361
396953361
可以看到問題已經解決了,但是這種實現方式的執行效率會很低。同步方法效率低,那我們考慮針對某些重要的程式碼進行單獨的同步,而不是全部進行同步:

public class LiuManSingleton {

  private static LiuManSingleton instance = null;

  private LiuManSingleton(){}

  public static LiuManSingleton getInstance() {
      if(instance == null){//懶漢式
        synchronized (LiuManSingleton.class) {
          instance = new LiuManSingleton();
        }
      }
      return instance;
  }
}

執行結果如下:
2036037666
396953361
396953361
396953361
396953361
396953361
396953361
396953361
396953361
396953361
從結果來看,這個單例模式雖然高效,但是並不是執行緒安全的,這是因為一開始多個執行緒都進入到了if判斷為true的程式碼塊裡面,之後才會發生執行緒阻塞,這個時候就會建立重複的例項。所以為了確保例項是單例的,我們需要雙重判斷:

public class LiuManSingleton {

  private static LiuManSingleton instance = null;

  private LiuManSingleton(){}

  public static LiuManSingleton getInstance() {
    if(instance == null){//懶漢式
      synchronized (LiuManSingleton.class) {
        if(null == instance){
          instance = new LiuManSingleton();
        }
      }
    }
    return instance;
  }
}

執行結果如下:
1431642599
1431642599
1431642599
1431642599
1431642599
1431642599
1431642599
1431642599
1431642599
1431642599
從執行結果來看,該中方法保證了多執行緒併發下的執行緒安全性。在同步程式碼塊中使用二次檢查,以保證其不被重複例項化。這種實現方式既保證了其高效性,也保證了執行緒安全性。所以在使用synchronized的時候我們需要慎重分析程式的設計,在實現執行緒安全的同時,使程式碼效率最大化。