SimpleDateFormat線程安全問題排查

一. 問題現象運營部門反饋使用小程序配置的拉新現金紅包活動二維碼,在掃碼后跳轉至404頁面 。
二. 原因排查

  1. 首先,檢查掃碼后的跳轉鏈接地址不是對應二維碼的實際URL,根據代碼邏輯推測,可能是accessToken在微信端已失效導致,檢查數據發現,數據庫存儲的accessToken過期時間為2022-11-29(排查問題當日為2022-10-08),發現過期時間太長 , 導致accessToken未刷新導致 。
  2. 接下來,繼續排查造成這一問題的真正原因 。排查日志發現更新sql語句對應的的過期時間與數據庫記錄的一致,推測賦值代碼存在問題,如下 。
tokenInfo.setExpireTime(simpleDateFormat.parse(token.getString("expireTime")));其中,simpleDateFormat在代碼中定義是該類的成員變量 。
  1. 跟蹤代碼后發現源碼中有明確說明SimpleDateFormat不應該應用于多線程場景下 。
Synchronization//SimpleDateFormat中的日期格式化不是同步的 。Date formats are not synchronized.//建議為每個線程創建獨立的格式實例 。It is recommended to create separate format instances for each thread.//如果多個線程同時訪問一個格式,則它必須保持外部同步 。If multiple threads access a format concurrently, it must be synchronized externally.
  1. 至此,基本可以判斷是simpleDateFormat.parse在多線程情況下造成錯誤的過期時間入庫,導致accesstoken無法正常更新 。
三. 原因分析
  1. 接下來寫個測試類來模擬:
@RunWith(SpringRunner.class)@SpringBootTestpublic class SimpleDateFormatTest {SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");/*** 定義線程池**/private static final ExecutorService threadPool = new ThreadPoolExecutor(16,20,0L,TimeUnit.MILLISECONDS,new LinkedBlockingDeque<>(1024),new ThreadFactoryBuilder().setNamePrefix("[線程]").build(),new ThreadPoolExecutor.AbortPolicy());@SneakyThrows@Testpublic void testParse() {Set<String> results = Collections.synchronizedSet(new HashSet<>());// 每個線程都對相同字符串執行“parse日期字符串”的操作,當THREAD_NUMBERS個線程執行完畢后,應該有且僅有一個相同的結果才是正確的String initialDateStr = "2022-10-08 18:30:01";for (int i = 0; i < 20; i++) {threadPool.execute(() -> {Date parse = null;try {parse = simpleDateFormat.parse(initialDateStr);} catch (ParseException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + "---" + parse);});}threadPool.shutdown();threadPool.awaitTermination(1, TimeUnit.HOURS);}}運行結果如下:
[線程]5---Sat Jan 08 18:30:01 CST 2000[線程]0---Wed Oct 08 18:30:01 CST 2200[線程]4---Sat Oct 08 18:30:01 CST 2022Exception in thread "[線程]3" 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:2089) 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.SimpleDateFormatTest.lambda$testParse$0(SimpleDateFormatTest.java:49) 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)[線程]6---Sat Oct 08 18:30:01 CST 2022[線程]11---Wed Mar 15 18:30:01 CST 2045Exception in thread "[線程]2" java.lang.ArrayIndexOutOfBoundsException: 275 at sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:453) at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2397) at java.util.GregorianCalendar.computeTime(GregorianCalendar.java:2818) at java.util.Calendar.updateTime(Calendar.java:3393) at java.util.Calendar.getTimeInMillis(Calendar.java:1782) at java.util.Calendar.getTime(Calendar.java:1755) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1532) at java.text.DateFormat.parse(DateFormat.java:364) at com.SimpleDateFormatTest.lambda$testParse$0(SimpleDateFormatTest.java:49) 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)[線程]6---Fri Oct 01 18:30:01 CST 8202[線程]12---Sat Oct 08 18:30:01 CST 2022Exception in thread "[線程]1" 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:2089) 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.SimpleDateFormatTest.lambda$testParse$0(SimpleDateFormatTest.java:49) 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)[線程]0---Sat Oct 08 18:30:01 CST 2022[線程]12---Sat Oct 08 18:30:01 CST 2022[線程]13---Sat Oct 08 18:30:01 CST 2022[線程]18---Sat Oct 08 18:30:01 CST 2022[線程]6---Sat Oct 01 18:30:01 CST 2022[線程]7---Sat Oct 08 18:30:01 CST 2022[線程]10---Sat Oct 08 18:30:01 CST 2022[線程]15---Sat Oct 08 18:00:01 CST 2022[線程]17---Sat Oct 08 18:30:01 CST 2022[線程]14---Sat Oct 08 18:30:01 CST 2022預期結果個數 1---實際結果個數7

推薦閱讀