SimpleDateFormat 的线程安全问题与 ThreadLocal

SimpleDateFormat 的线程安全说起

SimpleDateFormat 是 Java 中非常常用的一个类,用于解析和格式化日期字符串。这个类想必大家都有用过,但是 SimpleDateFormat 在多线程环境中并不是线程安全的。我刚知道这一点的时候也觉得很奇怪,因为 SimpleDateFormat 就是个工具类而已,为什么还会存在线程安全的问题呢。下面我们具体来看一下。

首先,我们写个简单的例子来验证一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;

public class DateUtil {
private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static String format(Date date) {
return dateFormat.format(date);
}

public static Date parse(String dateStr) throws ParseException {
return dateFormat.parse(dateStr);
}

public static void main(String[] args) {
final CountDownLatch latch = new CountDownLatch(1);
final String[] strs = new String[] {"2016-01-01 10:24:00", "2016-01-02 20:48:00", "2016-01-11 12:24:00"};
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}

for (int i = 0; i < 10; i++){
try {
System.out.println(Thread.currentThread().getName()+ "\t" + parse(strs[i % strs.length]));
Thread.sleep(100);
} catch (ParseException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
latch.countDown();
}
}

输出的部分内容是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Thread-9	Fri Jan 01 10:24:00 CST 2016
Thread-1 Sat Feb 25 00:48:00 CST 20162017
Thread-5 Sat Feb 25 00:48:00 CST 20162017
Exception in thread "Thread-4" Exception in thread "Thread-6" java.lang.NumberFormatException: For input string: "2002.E20022E"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
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:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at DateUtil.parse(DateUtil.java:24)
at DateUtil$2.run(DateUtil.java:45)

可以看到,其中的有些线程抛出了运行时异常 NumberFormatException,而有些则输出了奇怪的日期结果 20162017。很明显,在多线程环境中 SimpleDateFormat 确实存在线程安全的问题。

为什么 SimpleDateFormat 不是线程安全的?

在 JDK 的文档中提到了 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 的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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;
}

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

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

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

如何保证 SimpleDateFormat 的线程安全

最简单的方法就是每次要使用 SimpleDateFormat 时都创建一个局部的 SimpleDateFormat 对象。局部变量,自然就不存在线程安全的问题了。但如果需要频繁进行调用的话,每次都要创建新的对象,开销太大。

第二种方式,就是对 SimpleDateFormat 进行加锁,这样可以确保同一时间只有一个线程可以持有锁,进而解决线程安全的问题。但是这种方法在多线程竞争激烈的时候会带来效率问题。

第三种方式,就是使用 ThreadLocalThreadLocal 可以确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象,那么自然也就不存在竞争问题了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DateUtil {
private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};

public static String format(Date date) {
return local.get().format(date);
}

public static Date parse(String dateStr) throws ParseException {
return local.get().parse(dateStr);
}
}

ThreadLocal 来实现其实是有点类似于缓存的思路,每个线程都有一个独享的对象,避免了频繁创建对象,也避免了多线程的竞争。

也可以将 SimpleDateFormat 对象的创建进行延迟加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class DateUtil {
private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>();

private static SimpleDateFormat getDateFormat() {
SimpleDateFormat dateFormat = local.get();
if (dateFormat == null) {
dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
local.set(dateFormat);
}
return dateFormat;
}

public static String format(Date date) {
return getDateFormat().format(date);
}

public static Date parse(String dateStr) throws ParseException {
return getDateFormat().parse(dateStr);
}
}