高併發下的線程安全實現——互斥同步

好久沒來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的時候我們需要慎重分析程序的設計,在實現線程安全的同時,使代碼效率最大化。

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