一. 问题现象

运营部门反馈使用小程序配置的拉新现金红包活动二维码,在扫码后跳转至404页面。

二. 原因排查

  1. 首先,检查扫码后的跳转链接地址不是对应二维码的实际URL,根据代码逻辑推测,可能是accessToken在微信端已失效导致,检查数据发现,数据库存储的accessToken过期时间为2022-11-29(排查问题当日为2022-10-08),发现过期时间太长,导致accessToken未刷新导致。

  2. 接下来,继续排查造成这一问题的真正原因。排查日志发现更新sql语句对应的的过期时间与数据库记录的一致,推测赋值代码存在问题,如下。

  1. tokenInfo.setExpireTime(simpleDateFormat.parse(token.getString("expireTime")));

其中,simpleDateFormat在代码中定义是该类的成员变量。

  1. 跟踪代码后发现源码中有明确说明SimpleDateFormat不应该应用于多线程场景下。
  1. Synchronization
  2. //SimpleDateFormat中的日期格式化不是同步的。
  3. Date formats are not synchronized.
  4. //建议为每个线程创建独立的格式实例。
  5. It is recommended to create separate format instances for each thread.
  6. //如果多个线程同时访问一个格式,则它必须保持外部同步。
  7. If multiple threads access a format concurrently, it must be synchronized externally.
  1. 至此,基本可以判断是simpleDateFormat.parse在多线程情况下造成错误的过期时间入库,导致accesstoken无法正常更新。

三. 原因分析

  1. 接下来写个测试类来模拟:
  1. @RunWith(SpringRunner.class)
  2. @SpringBootTest
  3. public class SimpleDateFormatTest {
  4. SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  5. /**
  6. * 定义线程池
  7. **/
  8. private static final ExecutorService threadPool = new ThreadPoolExecutor(16,
  9. 20,
  10. 0L,
  11. TimeUnit.MILLISECONDS,
  12. new LinkedBlockingDeque<>(1024),
  13. new ThreadFactoryBuilder().setNamePrefix("[线程]").build(),
  14. new ThreadPoolExecutor.AbortPolicy()
  15. );
  16. @SneakyThrows
  17. @Test
  18. public void testParse() {
  19. Set<String> results = Collections.synchronizedSet(new HashSet<>());
  20. // 每个线程都对相同字符串执行“parse日期字符串”的操作,当THREAD_NUMBERS个线程执行完毕后,应该有且仅有一个相同的结果才是正确的
  21. String initialDateStr = "2022-10-08 18:30:01";
  22. for (int i = 0; i < 20; i++) {
  23. threadPool.execute(() -> {
  24. Date parse = null;
  25. try {
  26. parse = simpleDateFormat.parse(initialDateStr);
  27. } catch (ParseException e) {
  28. e.printStackTrace();
  29. }
  30. System.out.println(Thread.currentThread().getName() + "---" + parse);
  31. });
  32. }
  33. threadPool.shutdown();
  34. threadPool.awaitTermination(1, TimeUnit.HOURS);
  35. }
  36. }

运行结果如下:

  1. [线程]5---Sat Jan 08 18:30:01 CST 2000
  2. [线程]0---Wed Oct 08 18:30:01 CST 2200
  3. [线程]4---Sat Oct 08 18:30:01 CST 2022
  4. Exception in thread "[线程]3" java.lang.NumberFormatException: multiple points
  5. at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
  6. at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
  7. at java.lang.Double.parseDouble(Double.java:538)
  8. at java.text.DigitList.getDouble(DigitList.java:169)
  9. at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
  10. at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
  11. at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
  12. at java.text.DateFormat.parse(DateFormat.java:364)
  13. at com.SimpleDateFormatTest.lambda$testParse$0(SimpleDateFormatTest.java:49)
  14. at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
  15. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
  16. at java.lang.Thread.run(Thread.java:748)
  17. [线程]6---Sat Oct 08 18:30:01 CST 2022
  18. [线程]11---Wed Mar 15 18:30:01 CST 2045
  19. Exception in thread "[线程]2" java.lang.ArrayIndexOutOfBoundsException: 275
  20. at sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:453)
  21. at java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2397)
  22. at java.util.GregorianCalendar.computeTime(GregorianCalendar.java:2818)
  23. at java.util.Calendar.updateTime(Calendar.java:3393)
  24. at java.util.Calendar.getTimeInMillis(Calendar.java:1782)
  25. at java.util.Calendar.getTime(Calendar.java:1755)
  26. at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1532)
  27. at java.text.DateFormat.parse(DateFormat.java:364)
  28. at com.SimpleDateFormatTest.lambda$testParse$0(SimpleDateFormatTest.java:49)
  29. at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
  30. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
  31. at java.lang.Thread.run(Thread.java:748)
  32. [线程]6---Fri Oct 01 18:30:01 CST 8202
  33. [线程]12---Sat Oct 08 18:30:01 CST 2022
  34. Exception in thread "[线程]1" java.lang.NumberFormatException: multiple points
  35. at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
  36. at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
  37. at java.lang.Double.parseDouble(Double.java:538)
  38. at java.text.DigitList.getDouble(DigitList.java:169)
  39. at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
  40. at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
  41. at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
  42. at java.text.DateFormat.parse(DateFormat.java:364)
  43. at com.SimpleDateFormatTest.lambda$testParse$0(SimpleDateFormatTest.java:49)
  44. at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
  45. at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
  46. at java.lang.Thread.run(Thread.java:748)
  47. [线程]0---Sat Oct 08 18:30:01 CST 2022
  48. [线程]12---Sat Oct 08 18:30:01 CST 2022
  49. [线程]13---Sat Oct 08 18:30:01 CST 2022
  50. [线程]18---Sat Oct 08 18:30:01 CST 2022
  51. [线程]6---Sat Oct 01 18:30:01 CST 2022
  52. [线程]7---Sat Oct 08 18:30:01 CST 2022
  53. [线程]10---Sat Oct 08 18:30:01 CST 2022
  54. [线程]15---Sat Oct 08 18:00:01 CST 2022
  55. [线程]17---Sat Oct 08 18:30:01 CST 2022
  56. [线程]14---Sat Oct 08 18:30:01 CST 2022
  57. 预期结果个数 1---实际结果个数7

不仅有的线程结果不正确,甚至还有一些线程还出现了异常!

  1. 为什么SimpleDateFormat类不是线程安全的?

SimpleDateFormat继承了DateFormat,DateFormat内部有一个Calendar对象的引用,主要用来存储和SimpleDateFormat相关的日期信息。

SimpleDateFormat对parse()方法的实现。关键代码如下:

  1. @Override
  2. public Date parse(String text, ParsePosition pos) {
  3. ...省略中间代码
  4. Date parsedDate;
  5. try {
  6. ...
  7. parsedDate = calb.establish(calendar).getTime();
  8. } catch (IllegalArgumentException e) {
  9. ...
  10. }
  11. return parsedDate;
  12. }

establish()的实现如下:

  1. Calendar establish(Calendar cal) {
  2. ...省略中间代码
  3. cal.clear();
  4. for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
  5. for (int index = 0; index <= maxFieldIndex; index++) {
  6. if (field[index] == stamp) {
  7. cal.set(index, field[MAX_FIELD + index]);
  8. break;
  9. }
  10. }
  11. }
  12. ...
  13. return cal;
  14. }

在多个线程共享SimpleDateFormat时,同时也共享了Calendar引用,在如上代码中,calendar首先会进行clear()操作,然后进行set操作,在多线程情况下,set操作会覆盖之前的值,而且在后续对日期进行操作时,也可能会因为clear操作被清除导致异常。

四. 解决方案

  1. 将SimpleDateFormat定义成局部变量,每次使用时都new一个新对象,频繁创建对象消耗大,性能影响一些(JDK文档推荐此做法)
  1. public static Date parse(String strDate) throws ParseException {
  2. SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  3. return sdf.parse(strDate);
  4. }
  1. 维护一个SimpleDateFormat实体,转换方法上使用 Synchronized 保证线程安全:多线程堵塞(并发大系统不推荐)
  1. private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  2. public static String formatDate(Date date)throws ParseException{
  3. synchronized(sdf){
  4. return sdf.format(date);
  5. }
  6. }
  7. public static Date parse(String strDate) throws ParseException{
  8. synchronized(sdf){
  9. return sdf.parse(strDate);
  10. }
  11. }
  1. 使用ThreadLocal : 线程独享不堵塞,并且减少创建对象的开销(如果对性能要求比较高的情况,推荐这种方式)。
  1. public static ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(
  2. () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
  3. );
  4. public static Date parse(String strDate) throws ParseException {
  5. return threadLocal.get().parse(strDate);
  6. }
  1. DateTimeFormatter是Java8提供的新的日期时间API中的类,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用。
  1. String dateTimeStr= "2016-10-25 12:00:00";
  2. DateTimeFormatter formatter02 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
  3. LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStr,formatter02);
  4. System.out.println(localDateTime);
  5. String format = localDateTime.format(formatter02);
  6. System.out.println(format);
  7. 2016-10-25T12:00
  8. 2016-10-25 12:00:00

最终,我们根据实际情况公共包DateUtil类提供的strConvertDate方法,原理是按照方案1来解决该问题。

SimpleDateFormat线程安全问题排查的更多相关文章

  1. 解决SimpleDateFormat线程安全问题

    package com.tanlu.user.util; import java.text.DateFormat; import java.text.ParseException; import ja ...

  2. SimpleDateFormat线程安全问题

    今天线上出现了问题,从第三方获取的日期为 2019-12-12 11:11:11,通过SimpleDateFormat转换格式后,竟然出现完全不正常的日期数据,经百度,得知SimpleDateForm ...

  3. SimpleDateFormat的线程安全问题

    做项目的时候查询的日期总是不对,花了很长时间才找到异常的根源,原来SimpleDateFormat是非线程安全的,当我把这个类放到多线程的环境下转换日期就会出现莫名奇妙的结果,这种异常找出来可真不容易 ...

  4. SimpleDateFormat 的线程安全问题与解决方式

    SimpleDateFormat 的线程安全问题 SimpleDateFormat 是一个以国别敏感的方式格式化和分析数据的详细类. 它同意格式化 (date -> text).语法分析 (te ...

  5. SimpleDateFormat使用和线程安全问题

    SimpleDateFormat 是一个以国别敏感的方式格式化和分析数据的具体类. 它允许格式化 (date -> text).语法分析 (text -> date)和标准化. Simpl ...

  6. SimpleDateFormat时间格式化存在线程安全问题

    想必大家对SimpleDateFormat并不陌生.SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调 ...

  7. 转:浅谈SimpleDateFormat的线程安全问题

    转自:https://blog.csdn.net/weixin_38810239/article/details/79941964 在实际项目中,我们经常需要将日期在String和Date之间做转化, ...

  8. 关于SimpleDateFormat安全的时间格式化线程安全问题

    想必大家对SimpleDateFormat并不陌生.SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调 ...

  9. 044 SimpleDateFormat的线程安全问题与解决方案

    这个问题,以前好像写过,不过现在这篇文章,有一个重现的过程,还是值得读一读的. URL:SimpleDateFormat的线程安全问题与解决方案

随机推荐

  1. 1.3_HTML基础知识

    打开记事本,输入 <html> <hand> <title>我要自学网</title> </hand> <body> <h ...

  2. 记一次 .NET 某金融企业 WPF 程序卡死分析

    一:背景 1. 讲故事 前段时间遇到了一个难度比较高的 dump,经过几个小时的探索,终于给找出来了,在这里做一下整理,希望对大家有所帮助,对自己也是一个总结,好了,老规矩,上 WinDBG 说话. ...

  3. 简单创建一个SpringCloud2021.0.3项目(三)

    目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 上俩篇教程 3. Gateway集成sentinel,网关层做熔断降级 1. 超时熔断降级 2. 异常熔断 3. 集成sentine ...

  4. 大家都能看得懂的源码之ahooks useInfiniteScroll

    本文是深入浅出 ahooks 源码系列文章的第十七篇,该系列已整理成文档-地址.觉得还不错,给个 star 支持一下哈,Thanks. 简介 useInfiniteScroll 封装了常见的无限滚动逻 ...

  5. 《!--suppress ALL --> 在Android XML 文件中的用途是什么?

    <!--suppress ALL --> 在Android XML 文件中的用途是什么? 警告一次又一次地出现在谷歌地图的 XML 文件中,但是当我使用时,所有警告都被禁用.那么压制所有评 ...

  6. PHP之旅---出发(php+apache+MySQL)

    @ 目录 前言 准备 php安装 Apache安装 MySQL安装 Navicat安装(附) Apache+php整合 验证Apache+php 前言 本文详细介绍php+apache+MySQL在w ...

  7. 【疑难杂症】奇异值分解(SVD)原理与在降维中的应用

    前言 在项目实战的特征工程中遇到了采用SVD进行降维,具体SVD是什么,怎么用,原理是什么都没有细说,因此特开一篇,记录下SVD的学习笔记 参考:刘建平老师博客 https://www.cnblogs ...

  8. MinIO多租户(Multi-tenant)部署指南

    官方文档地址:http://docs.minio.org.cn/docs/master/multi-tenant-minio-deployment-guide 单机部署 在单台机器上托管多个租户,为每 ...

  9. 新版本中的hits.total匹配数说明

    在7.0版发布之前,hits.total始终用于表示符合查询条件的文档的实际数量.在Elasticsearch 7.0版中,如果匹配数大于10,000,则不会计算hits.total. 这是为了避免为 ...

  10. Kibana管理

    这里是用来管理您的 kibana 运行时配置的地方,包括初始化配置和后续的索引模式配置.高级设置等.您可以调整 kibana 自身的行为,也可以编辑您通过 kibana 保存的查询.视图.仪表板等各种 ...