1. 程式人生 > >非執行緒安全類SimpleDateFormat

非執行緒安全類SimpleDateFormat

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)