SimpleDateFormat線程安全問題深刻解析

背景

衆所周知,Java中的SimpleDateFormat不是線程安全的,在多線程下會出現意想不到的問題。本文將解析SimpleDateFormat線程不安全的具體緣由,從而加深對線程安全的理解。java

例子

簡單的測試代碼,當多個線程同時調用parse方法的時候會出問題:git

public class SimpleDateFormatTest {
    private static SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                try {
                    System.out.println(format.parse("2019/11/11 11:11:11"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

部分輸出以下:編程

Mon Nov 11 11:11:11 GMT 2019
Thu Jan 01 00:00:00 GMT 1970
java.lang.NumberFormatException: For input string: ""
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Long.parseLong(Long.java:601)
	at java.lang.Long.parseLong(Long.java:631)
	at java.text.DigitList.getLong(DigitList.java:195)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at package1.SimpleDateFormatTest.lambda$0(SimpleDateFormatTest.java:17)
	at package1.SimpleDateFormatTest
	at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: empty String
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
	at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
	at java.lang.Double.parseDouble(Double.java:538)
	at java.text.DigitList.getDouble(DigitList.java:169)
	at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
	at java.text.DateFormat.parse(DateFormat.java:364)
	at package1.SimpleDateFormatTest.lambda$0(SimpleDateFormatTest.java:17)
	at package1.SimpleDateFormatTest
	at java.lang.Thread.run(Thread.java:745)

不出意外,每次跑都會報錯,偶爾還會出現輸出初始時間Thu Jan 01 00:00:00 GMT 1970以及其餘莫名其妙的時間。好的,記住這兩個錯誤,下面咱們仔細分析。數組

分析

SimpleDateFormat繼承自DateFormat這個抽象類,UML圖以下: SimpleDateFormat UML緩存

DateFormat中有兩個全局變量須要注意安全

public abstract class DateFormat extends Format {

    //日曆變量,做爲DateFormat的輔助
    protected Calendar calendar;

    //用來Format數字,默認爲DecimalFormat
    protected NumberFormat numberFormat;
}

public class DecimalFormat extends NumberFormat {
    //DecimalFormat中的全局變量,用來存放轉化好的數據
    //digitList用科學技計數表示,如2019表示成0.2019x10^4
    private transient DigitList digitList = new DigitList();
}

這兩個變量的初始化在SimpleDateFormat的構造方法裏初始化。 看了類結構,咱們仔細分析一下DateFormatparse方法,直接上代碼(省略掉了一些可有可無的代碼):多線程

public Date parse(String text, ParsePosition pos)
{
    ......
    //注意這個變量calb,日期的轉化是經過CalendarBuilder這個類來完成的
    CalendarBuilder calb = new CalendarBuilder();

    //按照DateFormat的pattern逐個循環(年月日時分秒...)
    for (int i = 0; i < compiledPattern.length; ) {
        ......
        //最終調用subParse方法給calb賦值
        start = subParse(text, start, tag, count, obeyCount, ambiguousYear, pos, useFollowingMinusSignAsDelimiter, calb);
    }
    Date parsedDate;
    try {
        //調用CalendarBuilder的establish方法,把值傳遞給變量calendar
        //經過calendar來獲取最終返回的日期
        //注意,這裏calendar是個全局變量
        parsedDate = calb.establish(calendar).getTime();
    }
    ......

    return parsedDate;
}

主要分爲以下幾個步驟: > 1. 定義一個CalendarBuilder對象calb,用來臨時保存parse結果。 > 2. 根據DateFormat定義的Pattern,for循環調用subParse方法,將目標字符串逐個(年月日時分秒...)轉化,並存儲在calb變量裏。 > 3. 調用calb.establish(calendar)方法,把暫存在calb裏的數據設置到全局變量calendar裏。 > 4. 如今calendar裏已經包含轉換過的日期數據,最後調用**Calendar.getTime()**方法返回日期。併發

問題之一

下面看一下subParse方法裏面作了什麼,實現上有什麼問題。先看代碼(省略掉了一些可有可無的代碼):ide

public class SimpleDateFormat extends DateFormat {
    private int subParse(String text, int start, int patternCharIndex, int count,
                    boolean obeyCount, boolean[] ambiguousYear,
                    ParsePosition origPos,
                    boolean useFollowingMinusSignAsDelimiter, CalendarBuilder calb) {
        //一些變量初始化
        ......

        //內部調用numberFormat的parse方法,轉化數字
        //這裏的numberFormat就是上面分析過的那個全局變量,默認實例是DecimalFormat
        //text是代轉字符串"2019/11/11 11:11:11", pos是位置,如2019會被轉化爲0.2019x10^4
        number = numberFormat.parse(text, pos);
        if (number != null) {
            //轉化成int值,如0.2019x10^4會轉化成2019
            value = number.intValue();
        }
        int index;
        switch (patternCharIndex) {
        case PATTERN_YEAR:      // 'y'
            //有年,月,日等等各類case,這裏只拿PATTERN_YEAR(年)這種狀況舉例子
            //將numberFormat parse出來的值set到calb裏面去
            calb.set(field, value);
            return pos.index;
        }

        ......

        // 轉義失敗
        origPos.errorIndex = pos.index;
        return -1;
    }
}

//numberFormat.parse(text, pos)方法實現
public class DecimalFormat extends NumberFormat {

    public Number parse(String text, ParsePosition pos) {
        //內部調用subparse方法,將text的內容set到digitList上
        if (!subparse(text, pos, positivePrefix, negativePrefix, digitList, false, status)) {
            return null;
        }
        ......

        //將digitList轉變爲目標格式
        if (digitList.fitsIntoLong(status[STATUS_POSITIVE], isParseIntegerOnly())) {
            //parse爲Long型
            longResult = digitList.getLong();
        } else {
            //parse爲double型
            doubleResult = digitList.getDouble();
        }
        .....

        return gotDouble ? (Number)new Double(doubleResult) : (Number)new Long(longResult);
    }

    private final boolean subparse(String text, ParsePosition parsePosition,
                String positivePrefix, String negativePrefix,
                DigitList digits, boolean isExponent,
                boolean status[]) {
        //一些判斷及變量初始化準備
        ......

        //digitList在這個方法裏面叫digits,先對digits先清零處理。
        //decimalAt指小數點位置,如0.2019x10^4中decimalAt就是4
        //count指數字位數,如0.2019x10^4中count就是4
        digits.decimalAt = digits.count = 0;

        backup = -1;
        for (; position < text.length(); ++position) {
            //循環內部對digits一頓猛如虎的賦值操做,設置科學計數法各個部分的變量
            //注意這個digits是一個全局變量
            ......
        }

        //還要對digits繼續操做
        if (!sawDecimal) {
            digits.decimalAt = digitCount; // Not digits.count!
        }
        digits.decimalAt += exponent;

        ......
        return true;
    }
}

看到這裏,有點併發編程經驗的同窗估計就能看出問題了。在subparse這個方法裏面不加保護,當多個線程同時對全局變量digits(digitList)進行操做時,這個變量極可能是個無效的值。好比線程A把值設置了一半,另外一個線程B把值又清零初始化了。因而線程A在後面digitList.getDouble()digitList.getLong()的時候要麼獲得意料以外的值,要麼直接報錯NumberFormatException測試

問題之二

那麼後面的步驟有沒有問題呢?繼續往下看。 前面說到,方法會先把parse好的值放到CalendarBuilder型的臨時變量calb裏面,而後調用establish方法,將calb中緩存的值設置到SimpleDateFormatcalendar變量中,下面看看establish方法:

class CalendarBuilder {
    Calendar establish(Calendar cal) {
        ......
        //這個cal是SimpleDateFormat中的成員變量calendar
        //先將cal中的數據清除初始化,跟上面digitList同樣的套路
        cal.clear();
        
        for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
            for (int index = 0; index <= maxFieldIndex; index++) {
                if (field[index] == stamp) {
                    //前面CalendarBuild暫存的值都放在field數組裏,
                    //這裏將數組中的值逐個賦給cal
                    cal.set(index, field[MAX_FIELD + index]);
                    break;
                }
            }
        }

        if (weekDate) {
            //設置cal的weekdate field
            cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
        }
        return cal;
    }
}

仍是一樣的問題,因爲**calendar(cal)是個全局變量,當多個線程同時調用establish方法的時候,會有線程安全問題。舉個簡單的例子,線程A原先賦值好了"2019/11/11 11:11:11",結果線程B調用了cal.clear()**將數據又給清掉了,因而線程A回到瞭解放前,輸出了日期"1970/01/01 00:00:00"。

解決辦法

對於線程安全的解決辦法,給方法加同步synchronize是最簡單的,至關於線程只能一個一個地訪問parse方法:

synchronize (this) {
        System.out.println(format.parse("2019/11/11 11:11:11"));
    }

固然更common的使用姿式是配合ThreadLocal使用,至關於給每一個線程都定義了一個format變量,線程間互不影響:

private ThreadLocal<simpledateformat> format = new ThreadLocal<simpledateformat>(){  
        [@Override](https://my.oschina.net/u/1162528)  
        protected SimpleDateFormat initialValue() {  
            return new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");  
        }  
    };

    System.out.println(format.get().parse("2019/11/11 11:11:11"));

不過最推薦的仍是,不要用SimpleDateFormat,而是用Java8新引入的類LocalDateTime或者DateTimeFormatter,不只線程安全,並且效率更高。

總結

本文從代碼層面分析了SimpleDateFormat線程不安全的緣由。subparseestablish兩個方法均可能致使問題,前者還會拋出Exception。 總結下來,問題都是出在全局變量上。因此當咱們定義全局變量的時候必定要謹慎,注意變量是否是線程安全。</simpledateformat></simpledateformat>

相關文章
相關標籤/搜索