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

推荐阅读