衆所周知,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圖以下: 緩存
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的構造方法裏初始化。 看了類結構,咱們仔細分析一下DateFormat的parse方法,直接上代碼(省略掉了一些可有可無的代碼):多線程
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中緩存的值設置到SimpleDateFormat的calendar變量中,下面看看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線程不安全的緣由。subparse和establish兩個方法均可能致使問題,前者還會拋出Exception。 總結下來,問題都是出在全局變量上。因此當咱們定義全局變量的時候必定要謹慎,注意變量是否是線程安全。</simpledateformat></simpledateformat>