我们在对日期进行格式化时,除了用第三方的工具类,SimpleDateFormat应该是用的最多的。但大部分都是在单线程的环境下,那么SimpleDateFormat是线程安全的吗?我们首先写一个测试用例。
public class TestSimpleDateFormat {
public static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static void main(String[] args) {
for (int i=0; i<10; i++) {
Thread thread = new Thread(() -> {
try {
System.out.println(sdf.parse("2021-12-29 21:32:33"));
} catch (ParseException e) {
e.printStackTrace();
}
});
thread.start();
}
}
}
第一次执行并没有报错,第二次执行时结果如下:
java.lang.NumberFormatException: empty String
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
可见SimpleDateFormat不是线程安全的,在解释报错原因之前,我们先看一下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一个新对象,缺点就是频繁创建对象以及垃圾回收,影响系统性能
- 对方法加synchronized同步锁,缺点就是在高并发情况下性能较差
- 使用第三方库,比如joda-time,hutool等,将线程安全问题转移给第三方
- 使用ThreadLocal
使用ThreadLocal代码示例如下:
public class TestSimpleDateFormat {
public static ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
public static void main(String[] args) {
for (int i=0; i<10; i++) {
Thread thread = new Thread(() -> {
try {
System.out.println(threadLocal.get().parse("2021-12-29 21:32:33"));
} catch (ParseException e) {
e.printStackTrace();
}
});
thread.start();
}
}
}