SimpleDateFormat线程安全

SimpleDateFormat 是 Java 中非常常用的一个类,用于解析和格式化日期字符串,但是 SimpleDateFormat 在多线程环境中并不是线程安全的。

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.text.SimpleDateFormat;
public class Test {
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
private static class Task implements Runnable {
public void run() {
try {
System.out.println(sdf.parse("2016-03-21 12:00:00").getTime());
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Task task = new Task();
Thread t1 = new Thread(task);
t1.start();
Thread t2 = new Thread(task);
t2.start();
}
}

运行结果报错,如下:

1
2
3
4
5
6
7
8
java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1110)
at java.lang.Double.parseDouble(Double.java:540)
at java.text.DigitList.getDouble(DigitList.java:168)
at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1793)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)

原因

在 JDK 的文档中提到了 SimpleDateFormat 的线程安全问题:

1
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.

就是说DateFormat不是同步的,建议每个线程都分别创建format实例变量;或者如果多个线共享一个format的话,必须保持在使用format时是同步的.

源码分析

SimpleDateFormatformat()方法为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protected Calendar calendar;
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; ) {
..........
switch (tag) {
.........
default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}

format() 方法中先将日期存放到一个 Calendar 对象中,而这个 Calender对象在 SimpleDateFormat 中还是以成员变量存在的。在随后调用 subFormat() 时会再次用到成员变量 calendar。这就是引发问题的根源。在 parse() 方法中也会存在相应的问题。

在多线程环境下,如果两个线程都使用同一个 SimpleDateFormat 实例,那么就有可能存在其中一个线程修改了 calendar 后紧接着另一个线程也修改了 calendar,那么随后第一个线程用到 calendar 时已经不是它所期待的值了。

SimpleDateFormat 其实是有状态的,它使用一个 Calendar 成员变量来保存状态;如果要求 SimpleDateFormatparse()format() 是线程安全的,那么它其实应该是无状态的。将 Calendar 对象作为局部变量,内部在进行方法调用时每次都把它作为参数进行传递,其实就应该可以做到线程安全了。JDK 中 SimpleDateFormat 的实现之所以没有这样做可能是出于性能上的考虑,可以节约每次方法调用时都要创建 Calendar 对象的开销。但这种有状态的设计在某些场景下却反而带来了使用上的不便。

解决方案

创建局部变量

1
2
3
4
public Date formatDate(Date d) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.parse(d);
}

SimpleDateFormat 进行加锁

1
2
3
4
5
6
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
public Date formatDate(Date d) {
synchronized(sdf) {
return sdf.parse(d);
}
}

使用 ThreadLocal

1
2
3
4
5
6
7
8
9
private ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();
public Date formatDate(Date d) {
SimpleDateFormat sdf = tl.get();
if(sdf == null) {
sdf = new SimpleDateFormat("yyyy-MM-dd");
tl.set(sdf);
}
return sdf.parse(d);
}

使用 Joda-Time(推荐)

Joda-Time 是一个很棒的开源的 JDK 的日期和日历 API 的替代品,其 DateTimeFormat 是线程安全而且不变的。

1
2
3
4
5
6
7
8
9
10
public class DateFormatTest {
private final DateTimeFormatter fmt =
DateTimeFormat.forPattern("yyyyMMdd");
public Date convert(String source){
DateTime d = fmt.parseDateTime(source);
return.toDate();
}
}

参考

Java SimpleDateFormat的线程安全性问题

SimpleDateFormat 的线程安全问题与 ThreadLocal

热评文章