SimpleDateFormat的併發問題
java在做日期轉換時我們會使用SimpleDateFormat做時間轉換,但其實SimpleDateFormat不是執行緒安全的,如果SimpleDateFormat用static宣告或只例項化一次被多個執行緒使用併發度高時就會出併發異常,看如下例子
public static void main(String[] args) { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); List<String> lists = new ArrayList<String>() { { add("2018-11-22 01:11:11"); add("2018-11-22 02:22:22"); add("2018-11-22 03:33:33"); add("2018-11-22 04:44:44"); add("2018-11-22 03:55:55"); add("2018-11-22 04:55:56"); } }; ExecutorService executorService = Executors.newCachedThreadPool(); //用CountDownLatch增加併發度 CountDownLatch countDownLatch = new CountDownLatch(lists.size()); for (String list : lists) { executorService.submit(() -> { try { countDownLatch.await(); Date parse = format.parse(list); System.out.println(parse.toString()); } catch (Exception e) { e.printStackTrace(); } }); countDownLatch.countDown(); } }
執行日誌如下,可能每次報錯不一樣
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 thread.aqs.SimpleDateFormateTest.lambda$main$0(SimpleDateFormateTest.java:42) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) java.lang.NumberFormatException: For input string: "E2E2." at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043) 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 thread.aqs.SimpleDateFormateTest.lambda$main$0(SimpleDateFormateTest.java:42) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) java.lang.NumberFormatException: For input string: "E2E" at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043) 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 thread.aqs.SimpleDateFormateTest.lambda$main$0(SimpleDateFormateTest.java:42) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) java.lang.NumberFormatException: For input string: "E.4220118E4"
原因
SimpleDateFormat的父類持有一個物件Calendar
parse方法流程是這樣的:
//解析字串 ... //清空Calendar Calendar.clear(); //迴圈設定每個field ...
所以在多執行緒中會出現A執行緒設定Calendar值後被B執行緒修改,
或者A和B執行緒同時設定Calendar的值,都會出現錯誤
看一下原始碼
SimpleDateFormat繼承DateFormat
public class SimpleDateFormat extends DateFormat
入口
public Date parse(String source) throws ParseException { ParsePosition pos = new ParsePosition(0); Date result = parse(source, pos); if (pos.index == 0) throw new ParseException("Unparseable date: \"" + source + "\"" , pos.errorIndex); return result; }
parse中解析字串後的一部分
Date parsedDate; try { //出問題的方法 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; }
calb.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; } //清空 cal.clear(); for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) { for (int index = 0; index <= maxFieldIndex; index++) { if (field[index] == stamp) { //設定field 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); } return cal; }
如何解決
每次使用時都new一個SimpleDateFormat當然可以解決這個問題,但頻繁建立銷燬物件效能不高,方法上加鎖又會降低併發度
因為每個執行緒自己執行肯定是按順序執行,所以可以利用ThreadLocal
public class DateUtil{ private static ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(()->{ return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); }); public static Date parse(String dateStr) throws ParseException { return threadLocal.get().parse(dateStr); } public static String format(Date date) { return threadLocal.get().format(date); } }
每個執行緒持有自己的SimpleDateFormat,再跑測試程式碼,發現不報錯了
//輸出 Thu Nov 22 01:11:11 CST 2018 Thu Nov 22 04:55:56 CST 2018 Thu Nov 22 03:33:33 CST 2018 Thu Nov 22 03:55:55 CST 2018 Thu Nov 22 02:22:22 CST 2018 Thu Nov 22 04:44:44 CST 2018