Java多線程:SimpleDateFormat
一、SimpleDateFormat的線程安全問題
為什麽SimpleDateFormat是線程不安全的?
下面通過一個案例代碼來說明public class DateUtilTest {
public static class TestSimpleDateFormatThreadSafe extends Thread {
@Override
public void run() {
while(true) {
try {
this.join(2000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
try {
System.out.println(this.getName()+":"+DateUtil.parse("2013-05-24 06:02:20"));
} catch (ParseException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
for(int i = 0; i < 3; i++){
new TestSimpleDateFormatThreadSafe().start();
}
}
}
執行輸出如下:
Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java: 1082)
at java.lang.Double.parseDouble(Double.java:510)
at java.text.DigitList.getDouble(DigitList.java:151)
at java.text.DecimalFormat.parse(DecimalFormat.java:1302)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
at java.text.DateFormat.parse(DateFormat.java:335)
at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17)
at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20)
Exception in thread "Thread-0" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082)
at java.lang.Double.parseDouble(Double.java:510)
at java.text.DigitList.getDouble(DigitList.java:151)
at java.text.DecimalFormat.parse(DecimalFormat.java:1302)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
at java.text.DateFormat.parse(DateFormat.java:335)
at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17)
at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20)
Thread-2:Mon May 24 06:02:20 CST 2021
Thread-2:Fri May 24 06:02:20 CST 2013
說明:Thread-1和Thread-0報java.lang.NumberFormatException: multiple points錯誤,直接掛死,沒起來;Thread-2 雖然沒有掛死,但輸出的時間是有錯誤的,比如我們輸入的時間是:2017-05-24 06:02:20 ,當會輸出:Mon May 24 06:02:20 CST 2021 這樣的靈異事件
二.原因
作為一個專業程序員,我們當然都知道,相比於共享一個變量的開銷要比每次創建一個新變量要小很多。上面的優化過的靜態的SimpleDateFormat版,之所在並發情況下回出現各種靈異錯誤,是因為SimpleDateFormat和DateFormat類不是線程安全的。我們之所以忽視線程安全的問題,是因為從SimpleDateFormat和DateFormat類提供給我們的接口上來看,實在讓人看不出它與線程安全有何相幹。只是在JDK文檔的最下面有如下說明:
SimpleDateFormat中的日期格式不是同步的。推薦(建議)為每個線程創建獨立的格式實例。如果多個線程同時訪問一個格式,則它必須保持外部同步。
JDK原始文檔如下:
Synchronization:
Date formats are not synchronized.
It is recommended to create separate format instances for each thread.
If multiple threads access a format concurrently, it must be synchronized externally.
下面我們通過看JDK源碼來看看為什麽SimpleDateFormat和DateFormat類不是線程安全的真正原因:
SimpleDateFormat繼承了DateFormat,在DateFormat中定義了一個protected屬性的 Calendar類的對象:calendar。只是因為Calendar累的概念復雜,牽扯到時區與本地化等等,Jdk的實現中使用了成員變量來傳遞參數,這就造成在多線程的時候會出現錯誤。
在format方法裏,有這樣一段代碼:
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date);
boolean useDateFormatSymbols = useDateFormatSymbols();
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;
case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;
default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
calendar.setTime(date)這條語句改變了calendar,稍後,calendar還會用到(在subFormat方法裏),而這就是引發問題的根源。想象一下,在一個多線程環境下,有兩個線程持有了同一個SimpleDateFormat的實例,分別調用format方法:
線程1調用format方法,改變了calendar這個字段。
中斷來了。
線程2開始執行,它也改變了calendar。
又中斷了。
線程1回來了,此時,calendar已然不是它所設的值,而是走上了線程2設計的道路。如果多個線程同時爭搶calendar對象,則會出現各種問題,時間不對,線程掛死等等。
分析一下format的實現,我們不難發現,用到成員變量calendar,唯一的好處,就是在調用subFormat時,少了一個參數,卻帶來了這許多的問題。其實,只要在這裏用一個局部變量,一路傳遞下去,所有問題都將迎刃而解。
這個問題背後隱藏著一個更為重要的問題--無狀態:無狀態方法的好處之一,就是它在各種環境下,都可以安全的調用。衡量一個方法是否是有狀態的,就看它是否改動了其它的東西,比如全局變量,比如實例的字段。format方法在運行過程中改動了SimpleDateFormat的calendar字段,所以,它是有狀態的。
這也同時提醒我們在開發和設計系統的時候註意下一下三點:
1.自己寫公用類的時候,要對多線程調用情況下的後果在註釋裏進行明確說明
2.對線程環境下,對每一個共享的可變變量都要註意其線程安全性
3.我們的類和方法在做設計的時候,要盡量設計成無狀態的
三.解決辦法
1、使用同步
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);
}
}
}
說明:當線程較多時,當一個線程調用該方法時,其他想要調用此方法的線程就要block,多線程並發量大的時候會對性能有一定的影響。
2、使用ThreadLocal
public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
public static final String PATTERN_DATE_TIME = "yyyy-MM-dd HH:mm:ss";
private static Map<String, ThreadLocal<DateFormat>> dateFormatMap;
/**
* 創建單例
* 沒有使用單例模式的話,spring在註入數據時如果有邏輯調用了getDateFormat(pattern),會因為dateFormatMap尚未初始化而報NullPoint
* @return
*/
private static Map<String, ThreadLocal<DateFormat>> getDateFormatMap() {
if (dateFormatMap == null) {
synchronized (DateUtils.class) {
if (dateFormatMap == null) {
dateFormatMap = new ConcurrentHashMap<>();
}
}
}
return dateFormatMap;
}
/**
* 獲取線程安全的DateFormat
* SimpleDateFormat有兩個問題:
* 1.非線程安全,僅僅是聲明為static,使用時不上鎖在並發狀況下調用parse()有可能得到錯誤的時間
* 2.頻繁實例化有可能導致內存溢出
* 使用ThreadLocal將DateFormat變為線程獨享,既可以避免並發問題,又可以減少反復創建實例的開銷
* @param pattern
* @return
*/
public static DateFormat getDateFormat(final String pattern) {
ThreadLocal<DateFormat> dateFormat = getDateFormatMap().get(pattern);
if (dateFormat == null) {
dateFormat = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat(pattern);
}
};
dateFormatMap.put(pattern, dateFormat);
}
return dateFormat.get();
}
/**
* Date格式化 "yyyy-MM-dd HH:mm:ss"字符串
*
* @param date
* @return
*/
public static String DateFormatToString(Date date) {
DateFormat sdf = getDateFormat(PATTERN_DATE_TIME);
return sdf.format(date);
}
}
說明:使用ThreadLocal, 也是將共享變量變為獨享,線程獨享肯定能比方法獨享在並發環境中能減少不少創建對象的開銷。如果對性能要求比較高的情況下,一般推薦使用這種方法。
3.拋棄JDK,使用其他類庫中的時間格式化類
1.使用Apache commons 裏的FastDateFormat,宣稱是既快又線程安全的SimpleDateFormat, 可惜它只能對日期進行format, 不能對日期串進行解析。
2.使用Joda-Time類庫來處理時間相關問題
參考資料:
深入理解Java:SimpleDateFormat安全的時間格式化
SimpleDateFormat的線程安全問題與ThreadLocal
Java多線程:SimpleDateFormat