SimpleDateFormat是Java提供的一個格式化和解析日期的工具類,平常開發中應該常常會用到,可是因爲它是線程不安全的,多線程公用一個SimpleDateFormat實例對日期進行解析、格式化都會致使程序出錯,接下來就討論下它爲什麼是線程不安全的,以及如何避免。java
編寫測試代碼以下:git
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); public static void main(String[] args) { String[] waitingFormatTimeItems = { "2019-08-06", "2019-08-07", "2019-08-08" }; for (int i = 0; i < waitingFormatTimeItems.length; i++) { final int i2 = i; Thread thread = new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 100; j++) { String str = waitingFormatTimeItems[i2]; String str2 = null; Date parserDate = null; try { parserDate = sdf.parse(str); } catch (ParseException e) { e.printStackTrace(); } str2 = sdf.format(parserDate); System.out.println("i: " + i2 + "\tj: " + j + "\tThreadName: " + Thread.currentThread().getName() + "\t" + str + "\t" + str2); if (!str.equals(str2)) { throw new RuntimeException("date conversion failed after " + j + " iterations. Expected " + str + " but got " + str2); } } } }); thread.start(); } }
運行會拋出java.lang.RuntimeException,說明處理的結果時不正確的,從下邊日誌也看出來。安全
i: 2 j: 0 ThreadName: Thread-2 2019-08-08 2208-09-17 Exception in thread "Thread-2" Exception in thread "Thread-1" Exception in thread "Thread-0" i: 1 j: 0 ThreadName: Thread-1 2019-08-07 2208-09-17 i: 0 j: 0 ThreadName: Thread-0 2019-08-06 2208-09-17 java.lang.RuntimeException: date conversion failed after 0 iterations. Expected 2019-08-08 but got 2208-09-17 at dx.test.ThreadLocalTest$2.run(ThreadLocalTest.java:36) at java.lang.Thread.run(Thread.java:748) java.lang.RuntimeException: date conversion failed after 0 iterations. Expected 2019-08-07 but got 2208-09-17 at dx.test.ThreadLocalTest$2.run(ThreadLocalTest.java:36) at java.lang.Thread.run(Thread.java:748) java.lang.RuntimeException: date conversion failed after 0 iterations. Expected 2019-08-06 but got 2208-09-17 at dx.test.ThreadLocalTest$2.run(ThreadLocalTest.java:36) at java.lang.Thread.run(Thread.java:748)
測試代碼多運行幾回,會發現拋出 java.lang.NumberFormatException 異常:多線程
Exception in thread "Thread-1" Exception in thread "Thread-0" Exception in thread "Thread-2" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
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:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.dx.test.ThreadLocalTest$2.run(ThreadLocalTest.java:29)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
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:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.dx.test.ThreadLocalTest$2.run(ThreadLocalTest.java:29)
at java.lang.Thread.run(Thread.java:748)
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
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:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.dx.test.ThreadLocalTest$2.run(ThreadLocalTest.java:29)
at java.lang.Thread.run(Thread.java:748)
首先看下SimpleDateFormat的類圖結構:併發
從類圖和源代碼從均可以發現,SimpleDateFormat內部依賴於Calendar對象,經過下邊代碼分析會發現:實際上SimpleDateFormat的線程不安全就是由於Calendar是線程不安全的。ide
Calendar內部存儲的日期數據的變量field,time等都是不安全的,更重要的Calendar內部函數操做對變量操做是不具備原子性的操做。函數
SimpleDateFormat#parse方法:高併發
@Override public Date parse(String text, ParsePosition pos) { checkNegativeNumberExpression(); int start = pos.index; int oldStart = start; int textLength = text.length(); boolean[] ambiguousYear = {false}; //(1)解析日期字符串放入CalendarBuilder的實例calb中 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++]; } switch (tag) { case TAG_QUOTE_ASCII_CHAR: if (start >= textLength || text.charAt(start) != (char)count) { pos.index = oldStart; pos.errorIndex = start; return null; } start++; break; case TAG_QUOTE_CHARS: while (count-- > 0) { if (start >= textLength || text.charAt(start) != compiledPattern[i++]) { pos.index = oldStart; pos.errorIndex = start; return null; } start++; } break; default: // Peek the next pattern to determine if we need to obey the number of pattern letters for parsing. // It's required when parsing contiguous digit text (e.g., "20010704") with a pattern which has no delimiters between fields, like "yyyyMMdd". boolean obeyCount = false; // In Arabic, a minus sign for a negative number is put after the number. Even in another locale, a minus sign can be put after a number using DateFormat.setNumberFormat(). // If both the minus sign and the field-delimiter are '-', subParse() needs to determine whether a '-' after a number in the given text is a delimiter or is a minus sign for the preceding number. // We give subParse() a clue based on the information in compiledPattern. boolean useFollowingMinusSignAsDelimiter = false; if (i < compiledPattern.length) { int nextTag = compiledPattern[i] >>> 8; if (!(nextTag == TAG_QUOTE_ASCII_CHAR || nextTag == TAG_QUOTE_CHARS)) { obeyCount = true; } if (hasFollowingMinusSign && (nextTag == TAG_QUOTE_ASCII_CHAR || nextTag == TAG_QUOTE_CHARS)) { int c; if (nextTag == TAG_QUOTE_ASCII_CHAR) { c = compiledPattern[i] & 0xff; } else { c = compiledPattern[i+1]; } if (c == minusSign) { useFollowingMinusSignAsDelimiter = true; } } } start = subParse(text, start, tag, count, obeyCount, ambiguousYear, pos, useFollowingMinusSignAsDelimiter, calb); if (start < 0) { pos.index = oldStart; return null; } } } // At this point the fields of Calendar have been set. Calendar // will fill in default values for missing fields when the time // is computed. pos.index = start; Date parsedDate; try { //(2)使用calb中解析好的日期數據設置calendar parsedDate = calb.establish(calendar).getTime(); // If the year value is ambiguous, // then the two-digit year == the default start year if (ambiguousYear[0]) { if (parsedDate.before(defaultCenturyStart)) { parsedDate = calb.addYear(100).establish(calendar).getTime(); } } } // An IllegalArgumentException will be thrown by Calendar.getTime() // if any fields are out of range, e.g., MONTH == 17. catch (IllegalArgumentException e) { pos.errorIndex = start; pos.index = oldStart; return null; } return parsedDate; }
CalendarBuilder#establish方法:工具
Calendar establish(Calendar cal) { boolean weekDate = isSet(WEEK_YEAR) && field[WEEK_YEAR] > field[YEAR]; if (weekDate && !cal.isWeekDateSupported()) { // Use YEAR instead if (!isSet(YEAR)) { set(YEAR, field[MAX_FIELD + WEEK_YEAR]); } weekDate = false; } //(3)重置日期對象cal的屬性值 cal.clear(); //(4) 使用calb中中屬性設置cal // Set the fields from the min stamp to the max stamp so that // the field resolution works in the Calendar. for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) { for (int index = 0; index <= maxFieldIndex; index++) { if (field[index] == stamp) { cal.set(index, field[MAX_FIELD + index]); break; } } } if (weekDate) { int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1; int dayOfWeek = isSet(DAY_OF_WEEK) ? field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek(); if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) { if (dayOfWeek >= 8) { dayOfWeek--; weekOfYear += dayOfWeek / 7; dayOfWeek = (dayOfWeek % 7) + 1; } else { while (dayOfWeek <= 0) { dayOfWeek += 7; weekOfYear--; } } dayOfWeek = toCalendarDayOfWeek(dayOfWeek); } cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek); } //(5)返回設置好的cal對象 return cal; }
Calendar#clear()方法:性能
代碼(3)重置Calendar對象裏面的屬性值,以下代碼:
public final void clear() { for (int i = 0; i < fields.length; ) { stamp[i] = fields[i] = 0; // UNSET == 0 isSet[i++] = false; } areAllFieldsSet = areFieldsSet = false; isTimeSet = false; }
代碼(4)使用calb中解析好的日期數據設置cal對象
代碼(5) 返回設置好的cal對象
代碼(3)、(4)、(5)這幾步驟一塊兒操做不具備原子性,當A線程操做了(3)、(4),當將要執行(5)返回結果以前,若是B線程執行(3)會致使線程A的結果錯誤。
1)每一個線程使用時,都new一個SimpleDateFormat的實例,這保證每一個線程都用各自的Calendar實例。
public static void main(String[] args) { String[] waitingFormatTimeItems = { "2019-08-06", "2019-08-07", "2019-08-08" }; for (int i = 0; i < waitingFormatTimeItems.length; i++) { final int i2 = i; Thread thread = new Thread(new Runnable() { @Override public void run() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); for (int j = 0; j < 100; j++) { String str = waitingFormatTimeItems[i2]; String str2 = null; Date parserDate = null;try { parserDate = sdf.parse(str); } catch (ParseException e) { e.printStackTrace(); } str2 = sdf.format(parserDate); System.out.println("i: " + i2 + "\tj: " + j + "\tThreadName: " + Thread.currentThread().getName() + "\t" + str + "\t" + str2); if (!str.equals(str2)) { throw new RuntimeException("date conversion failed after " + j + " iterations. Expected " + str + " but got " + str2); } } } }); thread.start(); } }
這種方式缺點:每一個線程都 new 一個對象,而且使用後因爲沒有其它引用,都須要被回收,開銷比較大。
2)通過分析最終致使SimpleDateFormat的線程不安全緣由是步驟(3)、(4)、(5)不是一個原子性操做,那麼就能夠對其進行同步,讓(3)、(4)、(5)成爲原子操做,可使用ReetentLock。Synchronized等進行同步。
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); public static void main(String[] args) { String[] waitingFormatTimeItems = { "2019-08-06", "2019-08-07", "2019-08-08" }; for (int i = 0; i < waitingFormatTimeItems.length; i++) { final int i2 = i; Thread thread = new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < 100; j++) { String str = waitingFormatTimeItems[i2]; String str2 = null; Date parserDate = null; synchronized (sdf) { try { parserDate = sdf.parse(str); } catch (ParseException e) { e.printStackTrace(); } str2 = sdf.format(parserDate); } System.out.println("i: " + i2 + "\tj: " + j + "\tThreadName: " + Thread.currentThread().getName() + "\t" + str + "\t" + str2); if (!str.equals(str2)) { throw new RuntimeException("date conversion failed after " + j + " iterations. Expected " + str + " but got " + str2); } } } }); thread.start(); } }
使用了同步鎖,意味着多線程下會競爭鎖,在高併發狀況下會致使系統響應性能降低。
3)使用ThreadLocal,這樣每一個線程只須要使用一個SimpleDateFormat實例,在多線程下比第一種節省了對象的銷燬開銷,而且不須要對多線程進行同步,代碼以下:
當使用ThreadLocal維護變量時,ThreadLocal爲每一個使用該變量的線程提供獨立的變量副本,因此每個線程均可以獨立地改變本身的副本,而不會影響其餘線程所對應的副本。
ThreadLocal包含定義了一個ThreadLocalMap,ThreadLocalMap的key爲弱引用的線程(ThreadLocal<?>),要保存的線程局部變量的值爲value(Object).
private static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); }; }; public static void main(String[] args) { String[] waitingFormatTimeItems = { "2019-08-06", "2019-08-07", "2019-08-08" }; for (int i = 0; i < waitingFormatTimeItems.length; i++) { final int i2 = i; Thread thread = new Thread(new Runnable() { @Override public void run() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); for (int j = 0; j < 100; j++) { String str = waitingFormatTimeItems[i2]; String str2 = null; Date parserDate = null; try { parserDate = threadLocal.get().parse(str); } catch (ParseException e) { e.printStackTrace(); } str2 = threadLocal.get().format(parserDate); System.out.println("i: " + i2 + "\tj: " + j + "\tThreadName: " + Thread.currentThread().getName() + "\t" + str + "\t" + str2); if (!str.equals(str2)) { throw new RuntimeException("date conversion failed after " + j + " iterations. Expected " + str + " but got " + str2); } } } }); thread.start(); } }
參考: