2017.12.11SimpleDateFormat的線程安全性討論

轉載來自:http://blog.csdn.net/zxh87/article/details/19414885java

 

1.結論

DateFormat和SimpleDateFormat都不是線程安全的。在多線程環境中調用format()和parse()應處理線程安全的問題。緩存

 

2.錯誤示例

(1)錯誤示例1

每次處理一個時間信息,都新建一個SimpleDateFormat實例,而後再丟棄。形成內存的浪費。安全

 1 package com.peidasoft.dateformat;
 2 
 3 import java.text.ParseException;
 4 import java.text.SimpleDateFormat;
 5 import java.util.Date;
 6 
 7 public class DateUtil {
 8     
 9     public static  String formatDate(Date date)throws ParseException{
10         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
11         return sdf.format(date);
12     }
13     
14     public static Date parse(String strDate) throws ParseException{
15          SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
16         return sdf.parse(strDate);
17     }
18 }

 

(2)錯誤示例2

爲了防止頻繁建立,使用靜態的SimpleDateFormat實例,全部關於時間的處理都使用這個靜態實例。多線程

缺點是:在多線程環境中會有問題,好比轉換的時間不對,線程被掛死或者報奇怪的錯誤等等。都是由於SimpleDateFormat線程不安全形成的。併發

 1 package com.peidasoft.dateformat;
 2 
 3 import java.text.ParseException;
 4 import java.text.SimpleDateFormat;
 5 import java.util.Date;
 6 
 7 public class DateUtil {
 8     private static final  SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 9     
10     public static  String formatDate(Date date)throws ParseException{
11         return sdf.format(date);
12     }
13     
14     public static Date parse(String strDate) throws ParseException{
16         return sdf.parse(strDate);
17     }
18 }

 

3.源碼

JDK文檔中對於DateFormat的說明:app

SimpleDateFormat中的日期格式不是同步的。推薦(建議)爲每一個線程建立獨立的格式實例。若是多個線程同時訪問一個格式,則它必須保持外部同步。ide

1 Synchronization2   Date formats are not synchronized. 
3   It is recommended to create separate format instances for each thread. 
4   If multiple threads access a format concurrently, it must be synchronized externally.

 

SimpleDateFormat繼承了DateFormat。在DateFormat中定義了一個Calendar類的對象calendar。由於Calendar累的概念複雜,牽扯到時區與本地化等等,因此Jdk的實現中使用了成員變量來傳遞參數性能

format方法以下:學習

 1 private StringBuffer format(Date date, StringBuffer toAppendTo,
 2                                 FieldDelegate delegate) {
 4  calendar.setTime(date);
 6    boolean useDateFormatSymbols = useDateFormatSymbols();
 7 
 8    for (int i = 0; i < compiledPattern.length; ) {
 9         int tag = compiledPattern[i] >>> 8;
10         int count = compiledPattern[i++] & 0xff;
11         if (count == 255) {
12           count = compiledPattern[i++] << 16;
13           count |= compiledPattern[i++];
14         }
15 
16         switch (tag) {
17           case TAG_QUOTE_ASCII_CHAR:
18             toAppendTo.append((char)count);
19           break;
20 
21           case TAG_QUOTE_CHARS:
22             toAppendTo.append(compiledPattern, i, count);
23             i += count;
24           break;
25 
26           default:
27                 subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
28           break;
29         }
30     }
31         return toAppendTo;
32     }

 

calendar.setTime(date)這條語句改變了calendar,稍後,calendar還會用到(在subFormat方法裏),而這就是引起問題的根源。測試

在一個多線程環境下,有兩個線程持有了同一個SimpleDateFormat的實例,分別調用format方法:

  • 線程1調用format方法,改變了calendar這個字段。
  • 中斷。
  • 線程2開始執行,它也改變了calendar。
  • 又中斷。
  • 線程1回來了,此時,calendar已然不是它所設的值,再往下執行就可能會出現錯誤。

若是多個線程同時爭搶calendar對象,則會出現各類問題,時間不對,線程掛死等等。

 

這個問題背後隱藏着一個更爲重要的問題--無狀態。

無狀態方法的好處之一,就是它在各類環境下,均可以安全的調用。衡量一個方法是不是有狀態的,就看它是否改動了其它的東西,好比全局變量,好比實例的字段。format方法在運行過程當中改動了SimpleDateFormat的calendar字段,因此它是有狀態的。

這也同時提醒咱們在開發和設計系統的時候注意下一下三點:

  • 本身寫公用類的時候,要對多線程調用狀況下的後果在註釋裏進行明確說明。
  • 對線程環境下,對每個共享的可變變量都要注意其線程安全性。
  • 咱們的類和方法在作設計的時候,要儘可能設計成無狀態的。

 

4.解決辦法

(1)若是不特別考慮性能,能夠採用錯誤示例1中的用法,每用到一個SimpleDateFormat就新建一個

(2)若是考慮性能,想使用錯誤示例2中的形式,就須要採起額外的同步措施

 1 public class DateSyncUtil {
 2 
 3     private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
 4       
 5     public static String formatDate(Date date)throws ParseException{
 6         synchronized(sdf){  7             return sdf.format(date);
 8         }  
 9     }
10     
11     public static Date parse(String strDate) throws ParseException{
12         synchronized(sdf){
13             return sdf.parse(strDate);
14         }
15     } 
16 }

 

(3)若是要更加考慮性能,可使用ThreadLocal

使用ThreadLocal, 也是將共享變量變爲獨享,線程獨享確定能比方法獨享在併發環境中能減小很多建立對象的開銷。若是對性能要求比較高的狀況下,通常推薦使用這種方法。

寫法一:

 1 package com.peidasoft.dateformat;
 2 
 3 import java.text.DateFormat;
 4 import java.text.ParseException;
 5 import java.text.SimpleDateFormat;
 6 import java.util.Date;
 7 
 8 public class ConcurrentDateUtil {
 9 
10     private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
11         @Override
12         protected DateFormat initialValue() {
13             return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
14         }
15     };
16 
17     public static Date parse(String dateStr) throws ParseException {
18         return threadLocal.get().parse(dateStr);
19     }
20 
21     public static String format(Date date) {
22         return threadLocal.get().format(date);
23     }
24 }

 

寫法二:

 1 public class ThreadLocalDateUtil {
 2     private static final String date_format = "yyyy-MM-dd HH:mm:ss";
 3     private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(); 
4 public static DateFormat getDateFormat(){ 6 DateFormat df = threadLocal.get(); 7 if(df==null){ 8 df = new SimpleDateFormat(date_format); 9 threadLocal.set(df); 10 } 11 return df; 12 } 13 14 public static String formatDate(Date date) throws ParseException { 15 return getDateFormat().format(date); 16 } 17 18 public static Date parse(String strDate) throws ParseException { 19 return getDateFormat().parse(strDate); 20 } 21 }

 

5.測試和結論

作一個簡單的壓力測試,方法一最慢,方法三最快。

可是就算是最慢的方法一性能也不差,通常系統方法一和方法二就能夠知足,因此說在這個點很難成爲系統的瓶頸所在。從簡單的角度來講,建議使用方法一或者方法二,若是在必要的時候,追求那麼一點性能提高的話,能夠考慮用方法三,用ThreadLocal作緩存。

 

ps:Joda-Time類庫對時間處理方式比較完美,建議使用。(待學習)

相關文章
相關標籤/搜索