为何不用SimpleDateFormat?-- SimpleDateFormat非线性安全性简要分析
前言:每个Java项目,一般都会有一些工具类,其中的日期工具类,往往很容易看到SimpleDateFormat这个类的使用。一般我们是拿来做日期的格式化的,但是你有没有想过,这个类的一些方法是非线性安全的。
一. 案例
我们用static去修饰SimpleDateFormat
,代码里启动了10个线程,都用Test
类下的simpleDateFormat
属性去做日期格式化操作。
public class Test {
public static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
public static String[] dateStringArray = new String[]{"2021-01-01", "2021-01-02",
"2021-01-03", "2021-01-04", "2021-01-05", "2021-01-06",
"2021-01-07", "2021-01-08", "2021-01-09", "2021-01-10"};
@Test
public void testSimpleDateFormat() {
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
try {
String formatDate = simpleDateFormat.format(simpleDateFormat.parse(dateStringArray[finalI]));
if (!formatDate.equals(dateStringArray[finalI])) {
System.out.println(Thread.currentThread().getName() + "日期格式化出现问题!原日期:" + dateStringArray[finalI] + ",转换后日期:" + formatDate);
}
} catch (ParseException e) {
System.out.println(e);
}
}, "线程" + i).start();
}
}
}
看看结果会发生什么(部分截图):
我们可以从该案例中得出以下结论:
- 多线程情况下使用同一个
SimpleDateFormat
对象进行日期格式化可能会出现日期转换错误。 SimpleDateFormat
是非线性安全的。
二. 源码分析
首先,上述代码当中,核心的代码无非就是两个:
SimpleDateFormat.format()
SimpleDateFormat.parse()
那么我们来以format
方法为例,在这里我们只关注非线性安全的原因,而不去深入探究format
的运行机制。
1.点进方法,可以看见首先进入的是DateFormat
这个抽象类:
public abstract class DateFormat extends Format {
protected Calendar calendar;
public final String format(Date date){
return format(date, new StringBuffer(), DontCareFieldPosition.INSTANCE).toString();
}
public abstract StringBuffer format(Date date, StringBuffer toAppendTo,
FieldPosition fieldPosition);
}
calendar
这个属性,其注释的意思是:
用于计算时间的实例。该字段用于格式化和解析。
2.上述format
方法会调用当前类的抽象format
方法,最终实现调用的是SimpleDateFormat
子类下的format
方法。
public class SimpleDateFormat extends DateFormat {
// 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.format
方法,里面会先执行calendar.setTime(date);
,而这个calendar在父类DateFormat中被定义,但是我们可以看到,这个变量是一个共享变量,并且这个共享变量没有做线程安全控制。
2.1 共享变量
首先大家可以看下我写的这篇文章深入理解Java虚拟机系列(四)–Java内存模型和线程
我在这里将重要的内容贴出来:
Java内存模型规定:
- 所有的变量都存储在主内存中。
- 每条线程有属于自己的工作内存,其中保存了被该线程使用到的变量的主内存副本拷贝。
- 线程对变量的所有操作都必须在工作内存当中进行,而不能直接读写主内存中的变量。
- 不同的线程之间无法直接访问对方工作内存中的变量(私有),线程间变量值的传递需要通过主内存来完成。
那么简而言之,共享变量也就是所有线程可以进行访问和修改的这么一个变量。
那么,对于calendar.setTime(date);
这段代码而言,由于calendar
变量并没有做线程安全处理,因此在高并发情况下,可能会出现这种情况:
- 线程A调用
calendar.setTime(D1);
。 - 线程B调用
calendar.setTime(D2);
的时候,就会覆盖这个calendar
变量的值。 - 若线程A还没有跑后面的内容,那么此时此刻,对于线程A而言,明明是希望转换D1的值,却输出了D2的值。
- 因此就产生了线性安全问题。
三. 解决方案(依旧使用SimpleDateFormat)
上述问题的本质,也就是多个线程同时访问并修改了同一个对象的成员,导致出现了线性安全问题。那么我们可以这么修改代码:
3.1 每个线程都创建一个SimpleDateFormat
每个线程去创建属于自己的SimpleDateFormat
对象。
public class Test6 {
public SimpleDateFormat simpleDateFormat;
public static String[] dateStringArray = new String[]{"2021-01-01", "2021-01-02",
"2021-01-03", "2021-01-04", "2021-01-05", "2021-01-06",
"2021-01-07", "2021-01-08", "2021-01-09", "2021-01-10"};
@Test
public void testSimpleDateFormat() {
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
try {
simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
String formatDate = simpleDateFormat.format(simpleDateFormat.parse(dateStringArray[finalI]));
if (!formatDate.equals(dateStringArray[finalI])) {
System.out.println(Thread.currentThread().getName() + "日期格式化出现问题!原日期:" + dateStringArray[finalI] + ",转换后日期:" + formatDate);
}
} catch (ParseException e) {
System.out.println(e);
}
}, "线程" + i).start();
}
}
}
代码执行后,则并不会出现同样的问题:
3.2 使用ThreadLocal
public class Test6 {
public static ThreadLocal<SimpleDateFormat> t = new ThreadLocal<SimpleDateFormat>();
public static String[] dateStringArray = new String[]{"2021-01-01", "2021-01-02",
"2021-01-03", "2021-01-04", "2021-01-05", "2021-01-06",
"2021-01-07", "2021-01-08", "2021-01-09", "2021-01-10"};
public static SimpleDateFormat getSimpleDateFormat() {
SimpleDateFormat res;
res = t.get();
if (res == null) {
res = new SimpleDateFormat();
t.set(res);
}
return res;
}
@Test
public void testSimpleDateFormat() {
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
try {
SimpleDateFormat simpleDateFormat = getSimpleDateFormat();
String formatDate = simpleDateFormat.format(simpleDateFormat.parse(dateStringArray[finalI]));
if (!formatDate.equals(dateStringArray[finalI])) {
System.out.println(Thread.currentThread().getName() + "日期格式化出现问题!原日期:" + dateStringArray[finalI] + ",转换后日期:" + formatDate);
}
} catch (ParseException e) {
System.out.println(e);
}
}, "线程" + i).start();
}
}
}
后续
本篇文章还是针对于SimpleDateFormat
来讲的,并且只是针对SimpleDateFormat
来给出两种解决方案。问题是SimpleDateFormat
并不是日期格式化的一个很好选择,阿里开发规范中明确指出,禁止使用带static
修饰的SimpleDateFormat
变量。
那么如果不使用SimpleDateFormat
,我们如何进行日期的格式化相关操作呢?
我们可以通过LocalDateTime
、LocalDate
、LocalTime
来进行代替。他们是jdk8
引入的新类,准备在下一篇文章中介绍。