非執行緒安全類SimpleDateFormat
阿新 • • 發佈:2018-12-24
SimpleDateFormat是非執行緒安全的,寫處理日期的工具類時候請注意。
問題背景:
專案組的同事在新專案裡寫了一個DateUtil專門處理日期格式化的工具。線上執行後臺日誌偶然發生莫名其妙的錯誤:
java.lang.NumberFormatException: multiple points
java.lang.NumberFormatException: For input string: “”
java.lang.NumberFormatException: For input string: “.31023102EE22”
例如:
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1110)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat .subParse(SimpleDateFormat.java:1793)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
或者
java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java :65)
at java.lang.Long.parseLong(Long.java:453)
at java.lang.Long.parseLong(Long.java:483)
at java.text.DigitList.getLong(DigitList.java:194)
at java.text.DecimalFormat.parse(DecimalFormat.java:1316)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
原因分析:
根據錯誤日誌搜來問題程式碼:
public class DateUtil {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
...
...
public static Date parse(String strDate) throws ParseException{
return sdf.parse(strDate);
}
}
再分析專案呼叫該程式碼的場景:
Service A (執行緒1)某方法執行DateUtil.parse(“2017-02-12”)
Service B (執行緒2)某方法執行DateUtil.parse(“2017-03-12”)
並且當service A和service B同時觸發上面程式碼時候就出問題了。
JDK原始碼分析:
呼叫鏈:
SimpleDateFormat裡parse(String strDate)
=>DateFormat裡parse(source, pos);
public class SimpleDateFormat extends DateFormat {
...
transient private char[] compiledPattern;
...
...
public Date parse(String text, ParsePosition pos)
{
checkNegativeNumberExpression();
int start = pos.index;
int oldStart = start;
int textLength = text.length();
boolean[] ambiguousYear = {false};
CalendarBuilder calb = new CalendarBuilder();
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++];
} ...
...
...
在DateUtil裡變數SimpleDateFormat被定義為static,因而所有執行緒呼叫DateUtil時候都共享了該變數。
一看原始碼就直覺知道SimpleDateFormat是一個有狀態的物件了,因為它擁有很多成員變數,而且變數和很多方法都沒有加鎖同步處理。
例如狀態變數compiledPattern>>>8這句,假設多個執行緒同時修改該方法值,那各個執行緒間就互相影響了,從而SimpleDateFormat的parse方法肯定出問題。
解決方案:
- 去掉全域性靜態變數SimpleDateFormat,在每個parse方法裡new SimpleDateFormat
public class DateUtil {
...
...
public static Date parse(String strDate) throws ParseException{
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.parse(strDate);
}
}
- 在parse方法前加synchronized同步
public class DateUtil {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
...
...
public static synchronized Date parse(String strDate) throws ParseException{
return sdf.parse(strDate);
}
}
- 使用執行緒封閉的ThreadLocal實現同一執行緒內共享,不同執行緒間隔離 (此方法不推薦,詳細解釋留意下一篇文章詳解ThreadLocal)