背景:
最近又看到乱用SimpleDateFormat的情况,这里做个关于SimpleDateFormat多线程下的安全性问题的总结.
之前部门集合了一个时间工具类供大家使用,里面各式各样时间格式化的方法有几十上百个样子,然后由于很多方法都用的一个SimpleDateFormat,部门的机灵鬼发现这他娘不是重复代码嘛?然后就把他提出来了,提出来后后面也没发现什么问题,直到很久以后部门来了一个大流量的爬虫任务需要并发处理task,然后频繁调用时间格式化工具,然后在用这个SimpleDateFormat时候终于出现了问题,很多时间生成错乱
,甚至根本不是一个时间的样子,或者直接报错了.
1.问题复现
1.1模拟并发使用SimpleDateFormat
public class TimeConcurrErrorTest {
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
for (int i = 0; i <100 ; ++i) {
Thread thread = new Thread(()-> {
try {
System.out.println(sdf.parse("2020-12-11 11:17:27"));
} catch (ParseException e) {
e.printStackTrace();
}
});
thread.start();
}
}
}
1.2问题浮现
结果集如上图所示,部分时间格式转换没有出现报错,但是日期是千奇百怪的
,部分调用直接报错了
1.3问题排查
protected Calendar calendar;
// Called from Format after creating a FieldDelegate
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date);
boolean useDateFormatSymbols = useDateFormatSymbols();
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;
case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;
default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
SimpleDateFormat(下面简称sdf)类内部有一个Calendar对象引用,它用来储存和这个sdf相关的日期信息,例如sdf.parse(dateStr), sdf.format(date) 诸如此类的方法参数传入的日期相关String, Date等等, 都是交友Calendar引用来储存的.这样就会导致一个问题,如果你的sdf是个static的, 那么多个thread 之间就会共享这个sdf, 同时也是共享这个Calendar引用, 并且, 观察 sdf.parse() 方法,你会发现有如下的调用:
Date parse() {
calendar.clear(); // 清理calendar
calendar.setTime(); // 设置calendar的时间
... // 执行一些操作
calendar.getTime(); // 获取calendar的时间
}
这里会导致的问题就是, 如果 线程A 调用了 sdf.parse(), 并且进行了 calendar.clear()后还未执行calendar.getTime()的时候,线程B又调用了sdf.parse(), 这时候线程B也执行了sdf.clear()方法, 这样就导致线程A的的calendar数据被清空了(实际上A,B的同时被清空了). 又或者当 A 执行了calendar.clear() 后被挂起, 这时候B 开始调用sdf.parse()并顺利i结束, 这样 A 的 calendar内存储的的date 变成了后来B设置的calendar的date.亦或是并发setTime()等等问题.
这就造成了多线程并发修改的问题
2.问题解决
1.每次方法调用的时候都使用创建一个新的SimpleDateFormat自己用
缺点:如果我们同一线程多次调用格式化方法岂不是创建销毁了很多次SimpleDateFormat?? 并发下一点点资源的损耗都会造成积少成多的情况,所以我们尽量减少重复资源的占用.这种方案可行但是不太好
2.对于单一线程频繁使用SimpleDateFormat的,可以使用ThreadLocal存储用时再取即可
3.使用java8提供的更安全的LocalDateTime (推荐!
)
核心思想:基于领域模型驱动设计方法以及不可变类,提供了各种各样的安全类,比如做时间差的Duration,还有LocalDate,LocalTime,LocalDateTime等不可变类,并提供了相互的转换方法
优点:
- 1.date有的LocalDateTime
都有
,有非常非常强大的Api,我也基于他的api封装了一些工具类,但是公司代码不好提供,大家可以直接参阅文档 - 2.
安全可靠