1. 程式人生 > >高併發下的執行緒安全實現——執行緒區域性變數

高併發下的執行緒安全實現——執行緒區域性變數

今天我們來討論另外一種執行緒安全的實現方法。如果說互斥同步是多執行緒間的資料共享,那麼執行緒區域性變數就是執行緒間的資料隔離。ThreadLocal把共享資料的可見範圍限制在同一個執行緒之內,這樣無須同步也能實現執行緒之間資料不爭用的問題。當使用ThreadLocal維護變數時,ThreadLocal為每個使用該變數的執行緒提供獨立的變數副本,所以每一個執行緒都可以獨立地改變自己的副本,而不會影響其它執行緒所對應的副本。執行緒區域性變數實現的原理也比較簡單:每個執行緒的Thread物件中都有一個ThreadLocalMap物件,這個map儲存了一組以該執行緒所對應的雜湊碼ThreadLocal.threadLocalHashCode為鍵,以執行緒區域性變數為值的K-V值對,每一個執行緒物件都包含了一個獨一無二的threadLocalHashCode值,使用這個值就可以獲取相對應的變量了,這個變數就是上文中提到的執行緒區域性變數的副本。
我們來分析一下java中最常用到的日期時間格式化工具類SimpleDateFormat。SimpleDateFormat類用來對日期字串進行解析和格式化輸出,但如果使用不小心會導致非常微妙和難以除錯的問題,因為 DateFormat 和 SimpleDateFormat 類都不是執行緒安全的,以下程式碼:

public class DateUtil {
  private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  public static String formatDate(Date date) throws ParseException {
    return sdf.format(date);
  }

  public static Date parse(String strDate) throws ParseException {
    return sdf.parse(strDate);
  }
}

沒有使用任何同步手段來確保執行緒安全,我們使用一個測試用例來測試一下是否執行正常:

@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(Thread.currentThread().getName()+":"+DateUtil.parse("2017-06-24 06:02:20")); Thread.sleep(30000); } catch (Exception e) { System.out.println(e.getMessage()); } } }; service.execute(runnable);// 為執行緒池新增任務 } }

執行結果為:
pool-1-thread-2:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-7:Sat Jun 24 06:02:02 CST 2017
pool-1-thread-3:Sat Jun 24 06:02:02 CST 2017
pool-1-thread-6:Tue Jul 31 06:02:20 CST 2018
pool-1-thread-8:Tue Jul 24 06:02:20 CST 2018
pool-1-thread-4:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-9:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-5:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-1:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-10:Sat Jun 24 06:02:20 CST 2017
很明顯,執行緒6和執行緒8輸出的時間是有錯誤的,這是因為SimpleDateFormat和DateFormat類不是執行緒安全的。在多執行緒環境下呼叫 format() 和 parse() 方法應該使用同步程式碼來避免問題。以下程式碼:

public class DateSyncUtil{
  private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  public static String formatDate(Date date)throws ParseException{
      synchronized(sdf){
          return sdf.format(date);
      }  
  }

  public static Date parse(String strDate) throws ParseException{
      synchronized(sdf){
          return sdf.parse(strDate);
      }
  }
}

我們使用一個測試用例來測一下看是否執行正常:

public class SyncTest {

  @Test
  public void test01(){
      ExecutorService service = Executors.newCachedThreadPool(); // 建立一個執行緒池
      final CountDownLatch cdOrder = new CountDownLatch(1);
      for (int i = 0; i < 10; i++) {
        Runnable runnable = new Runnable() {
          public void run() {
            try {
              System.out.println(Thread.currentThread().getName()+":"+DateSyncUtil.parse("2017-06-24 06:02:20"));
              cdOrder.await();
            } catch (Exception e) {
              e.printStackTrace();
            }
          }
        };
        service.execute(runnable);// 為執行緒池新增任務
      }
  }
}

執行結果:
pool-1-thread-9:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-3:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-4:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-6:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-1:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-2:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-5:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-10:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-8:Sat Jun 24 06:02:20 CST 2017
通過結果可以看出以上程式碼肯定是執行緒安全的,可以保證資料的正確性。但是在高併發環境下,當一個執行緒呼叫該方法時,其他想要呼叫此方法的執行緒就要阻塞,多執行緒併發量大的時候會對效能有一定的影響。如果系統對效能有比較高的要求,那麼推薦使用ThreadLocal來隔離資料在一個執行緒中:

public class ConcurrentDateUtil {

  private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
    @Override
    protected DateFormat initialValue() {
      return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
  };

  public static Date parse(String dateStr) throws ParseException {
    return threadLocal.get().parse(dateStr);
  }

  public static String format(Date date) {
    return threadLocal.get().format(date);
  }
}

呼叫上面的測試用例測試的結果如下:
pool-1-thread-4:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-8:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-3:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-1:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-6:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-9:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-2:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-5:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-10:Sat Jun 24 06:02:20 CST 2017
pool-1-thread-7:Sat Jun 24 06:02:20 CST 2017
使用ThreadLocal,也是將共享變數變為獨享,執行緒獨享肯定能比方法獨享在併發環境中能減少不少建立物件的開銷。
雖然ThreadLocal相比於互斥同步在時間效能上面有一定的優勢,但是需要注意它們兩者所應用的場景,ThreadLocal用於資料隔離,即當很多執行緒需要多次使用同一個物件,並且需要該物件具有相同初始化值的時候最適合使用ThreadLocal。而synchronized用於資料同步,即當多個執行緒需要訪問或修改某個物件的時候使用synchronized來阻塞其他執行緒從而只允許一個執行緒訪問或修改該物件。