SimpleDateFormat线程安全问题排查
一. 问题现象
运营部门反馈使用小程序配置的拉新现金红包活动二维码,在扫码后跳转至404页面。
二. 原因排查
首先,检查扫码后的跳转链接地址不是对应二维码的实际URL,根据代码逻辑推测,可能是accessToken在微信端已失效导致,检查数据发现,数据库存储的accessToken过期时间为2022-11-29(排查问题当日为2022-10-08),发现过期时间太长,导致accessToken未刷新导致。
接下来,继续排查造成这一问题的真正原因。排查日志发现更新sql语句对应的的过期时间与数据库记录的一致,推测赋值代码存在问题,如下。
tokenInfo.setExpireTime(simpleDateFormat.parse(token.getString("expireTime")));
其中,simpleDateFormat在代码中定义是该类的成员变量。
- 跟踪代码后发现源码中有明确说明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.
- 至此,基本可以判断是simpleDateFormat.parse在多线程情况下造成错误的过期时间入库,导致accesstoken无法正常更新。
三. 原因分析
- 接下来写个测试类来模拟:
@RunWith(SpringRunner.class)
@SpringBootTest
public 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
@Test
public 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 2022
Exception 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 2045
Exception 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 2022
Exception 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
不仅有的线程结果不正确,甚至还有一些线程还出现了异常!
- 为什么SimpleDateFormat类不是线程安全的?
SimpleDateFormat继承了DateFormat,DateFormat内部有一个Calendar对象的引用,主要用来存储和SimpleDateFormat相关的日期信息。
SimpleDateFormat对parse()方法的实现。关键代码如下:
@Override
public Date parse(String text, ParsePosition pos) {
...省略中间代码
Date parsedDate;
try {
...
parsedDate = calb.establish(calendar).getTime();
} catch (IllegalArgumentException e) {
...
}
return parsedDate;
}
establish()的实现如下:
Calendar establish(Calendar cal) {
...省略中间代码
cal.clear();
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;
}
}
}
...
return cal;
}
在多个线程共享SimpleDateFormat时,同时也共享了Calendar引用,在如上代码中,calendar首先会进行clear()操作,然后进行set操作,在多线程情况下,set操作会覆盖之前的值,而且在后续对日期进行操作时,也可能会因为clear操作被清除导致异常。
四. 解决方案
- 将SimpleDateFormat定义成局部变量,每次使用时都new一个新对象,频繁创建对象消耗大,性能影响一些(JDK文档推荐此做法)
public static Date parse(String strDate) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(strDate);
}
- 维护一个SimpleDateFormat实体,转换方法上使用 Synchronized 保证线程安全:多线程堵塞(并发大系统不推荐)
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date)throws ParseException{
synchronized(sdf){
return sdf.format(date);
}
}
public static Date parse(String strDate) throws ParseException{
synchronized(sdf){
return sdf.parse(strDate);
}
}
- 使用ThreadLocal : 线程独享不堵塞,并且减少创建对象的开销(如果对性能要求比较高的情况,推荐这种方式)。
public static ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
public static Date parse(String strDate) throws ParseException {
return threadLocal.get().parse(strDate);
}
- DateTimeFormatter是Java8提供的新的日期时间API中的类,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用。
String dateTimeStr= "2016-10-25 12:00:00";
DateTimeFormatter formatter02 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStr,formatter02);
System.out.println(localDateTime);
String format = localDateTime.format(formatter02);
System.out.println(format);
2016-10-25T12:00
2016-10-25 12:00:00
最终,我们根据实际情况公共包DateUtil类提供的strConvertDate方法,原理是按照方案1来解决该问题。
SimpleDateFormat线程安全问题排查的更多相关文章
- 解决SimpleDateFormat线程安全问题
package com.tanlu.user.util; import java.text.DateFormat; import java.text.ParseException; import ja ...
- SimpleDateFormat线程安全问题
今天线上出现了问题,从第三方获取的日期为 2019-12-12 11:11:11,通过SimpleDateFormat转换格式后,竟然出现完全不正常的日期数据,经百度,得知SimpleDateForm ...
- SimpleDateFormat的线程安全问题
做项目的时候查询的日期总是不对,花了很长时间才找到异常的根源,原来SimpleDateFormat是非线程安全的,当我把这个类放到多线程的环境下转换日期就会出现莫名奇妙的结果,这种异常找出来可真不容易 ...
- SimpleDateFormat 的线程安全问题与解决方式
SimpleDateFormat 的线程安全问题 SimpleDateFormat 是一个以国别敏感的方式格式化和分析数据的详细类. 它同意格式化 (date -> text).语法分析 (te ...
- SimpleDateFormat使用和线程安全问题
SimpleDateFormat 是一个以国别敏感的方式格式化和分析数据的具体类. 它允许格式化 (date -> text).语法分析 (text -> date)和标准化. Simpl ...
- SimpleDateFormat时间格式化存在线程安全问题
想必大家对SimpleDateFormat并不陌生.SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调 ...
- 转:浅谈SimpleDateFormat的线程安全问题
转自:https://blog.csdn.net/weixin_38810239/article/details/79941964 在实际项目中,我们经常需要将日期在String和Date之间做转化, ...
- 关于SimpleDateFormat安全的时间格式化线程安全问题
想必大家对SimpleDateFormat并不陌生.SimpleDateFormat 是 Java 中一个非常常用的类,该类用来对日期字符串进行解析和格式化输出,但如果使用不小心会导致非常微妙和难以调 ...
- 044 SimpleDateFormat的线程安全问题与解决方案
这个问题,以前好像写过,不过现在这篇文章,有一个重现的过程,还是值得读一读的. URL:SimpleDateFormat的线程安全问题与解决方案
随机推荐
- 【小白必看】Redis手把手教你从零开始下载到安装,再到配置允许图形化工具远程连接(一)
一.Redis安装 本文暂时仅介绍Windows环境下Redis的安装. 由于Windows环境下没有.exe安装文件,这里我们使用"曲线救国"的.msi安装包帮助我们一站式解决安 ...
- MixCSE:困难样本在句子表示中的使用
Unsupervised Sentence Representation via Contrastive Learning with Mixing Negatives 论文地址:https://www ...
- QFile 对文件进行读写操作
QFile 对文件进行读写操作 1 QFile 进行读写操纵 2 QFile file(pah ) 文件路径 3 读 file.open(打开方式) file.readAll(). file.re ...
- CodeForces - 1625C
Problem - 1625C - Codeforces 题意: 一条马路,有n个限速牌,表示的是从这个限速牌开始到下一个限速牌或者到马路尾的这段距离的速度,你可以拆除其中k个限速牌,问最少的时间是多 ...
- Macos下用pycharm运行django项目死活安装不上mysqlclient怎么办!!??
花了我三天时间,佛了 我刚从win过渡到mac,想着把代码迁移一下. 然后看到依赖里面有一个mysqlclient,然后pip3 install死活装不上 解决方案: 在这里写上这个 然后就好,死了
- Windows编程之线程同步
本笔记整理自:<Windows核心编程(第五版)> 目录 什么是线程同步 用户方式中的线程同步 原子访问:Interlocked系列函数 CRITICAL_SECTION:关键段 内核对象 ...
- LFS(Linux From Scratch)构建过程全记录(六):交叉编译临时工具
写在前面 本章将展示如何使用刚刚构建的跨工具链来交叉编译基本实用程序. M4安装 和前文一样,先进行解压,然后cd进入 注意:不需要构建build文件夹,直接输入以下配置文件 ./configure ...
- 《Java基础——循环语句》
Java基础--循环语句 1. while语句: 规则: 1. 首先计算表达式的值. 2. 若表达式为真,则执行循环语法,直至表达式为假,循环结束. 格式: while(表达式) 语句 ...
- Flink基础概念入门
Flink 概述 什么是 Flink Apache Apache Flink 是一个开源的流处理框架,应用于分布式.高性能.高可用的数据流应用程序.可以处理有限数据流和无限数据,即能够处理有边界和无边 ...
- Traefik2.X 版本 中 URL Rewrite 的使用
文章转载自:https://mp.weixin.qq.com/s?__biz=MzU4MjQ0MTU4Ng==&mid=2247484594&idx=1&sn=becbe567 ...