谈谈 SimpleDateFormat 使用的注意事项

来看看SimpleDateFormat的线程安全问题。

程序猿日常与时间打交道是必不可少的,而针对date类型的转换,用的最多的应该就是SimpleDateFormat,然而,此类并不是线程安全的,此文会列出证明其线程不安全和其原因,以及如何解决。

SimpleDateFormat多线程任务线程不安全

来看日常使用SimpDateFormat的简单代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SimpDateFormatTest {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

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

public static Date parse(String timeStr){
Date date = new Date();
try {
date = sdf.parse(timeStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}

public static void main(String[] args) {
parse("2019-04-11 13:48:08");
}
}

单线程情况下没有什么问题,来看多线程任务下的执行情况,将结果打印到控制台:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(100);
for (int i = 0; i < 20; i++) {
service.execute(() -> {
System.out.println(parse("2019-04-11 10:48:08"));
});
}
service.shutdown();
service.awaitTermination(1,TimeUnit.SECONDS);
}

这里用20个线程来做String与Date的转换,看看控制台:

java-simpledateformat

可以看到,多线程任务执行下转换的日期的不匹配,没毛病吧,再看看format方法多线程环境下的打印:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws InterruptedException {
Date date = parse("1998-04-11 19:00:00");
Date date1 = parse("2019-04-11 10:00:00");
ExecutorService service = Executors.newFixedThreadPool(100);
for (int i = 0; i < 10; i++) {
final int j = i;
service.execute(() -> {
if (j % 2 == 0) {
System.out.println(formatDate(date));
}else {
System.out.println(formatDate(date1));
}
});
}
service.shutdown();
service.awaitTermination(1,TimeUnit.SECONDS);
}

控制台打印如下:

java-simpledateformat1

你看2019多过分,平白无故比人家1998多打印3次,值大压值小吗?肯定不是,来看看原因。

为何非线程安全

因为把SimpleDateFormat定义为静态变量,那么多线程下SimpleDateFormat的实例就会被多个线程共享,B线程会读取到A线程的时间,就会出现时间差异和其它各种问题。SimpleDateFormat和它继承的DateFormat类也不是线程安全的

来看看SimpleDateFormat的format()方法的源码

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
// 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;
}

这里需要注意的是calendar.setTime(date);,SimpleDateFormat的format方法实际操作的就是Calendar。

因为声明SimpleDateFormat为static变量,那么它的Calendar变量也就是一个共享变量,可以被多个线程访问。

假设线程A执行完calendar.setTime(date),把时间设置成1998-04-11 19:00:00,这时候被挂起,线程B获得CPU执行权。线程B也执行到了calendar.setTime(date),把时间设置为2019-04-11 10:00:00。线程挂起,线程A继续走,calendar还会被继续使用(subFormat方法),而这时calendar用的是线程B设置的值了,而这就是引发问题的根源,出现时间不对,线程挂死等等。

SimpleDateFormat上面给出的注释也明确的指出:

1
2
3
4
* 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.

如何解决

  • 不在类中用static修饰,直接把SimpleDateFormat放到方法中来创建实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static String formatDate(Date date){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}

public static Date parse(String timeStr){
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date();
try {
date = sdf.parse(timeStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}

如上代码,仅在需要用到的地方创建一个新的实例,就没有线程安全问题,不过也加重了创建对象的负担,会频繁地创建和销毁对象,效率较低。

  • synchronized简单粗暴
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static String formatDate(Date date){
synchronized (sdf) {
return sdf.format(date);
}
}

public static Date parse(String timeStr){
synchronized (sdf) {
Date date = new Date();
try {
date = sdf.parse(timeStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}

简单粗暴,synchronized往上一套也可以解决线程安全问题,缺点自然就是并发量大的时候会对性能有影响,线程阻塞。

  • ThreadLocal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};

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

public static Date parse(String timeStr){
Date date = new Date();
try {
date = threadLocal.get().parse(timeStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}

ThreadLocal会为每一个被其修饰的变量创建一个副本在线程中使用,因此可以解决线程安全问题,缺点是占用内存,当然这点内存对java来讲貌似可以忽略不计。

  • 基于JDK1.8的DateTimeFormatter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private static final DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

public static String formatDate(LocalDateTime date){
return format.format(date);
}

public static LocalDateTime parse(String timeStr){
return LocalDateTime.parse(timeStr,format);
}

public static void main(String[] args) throws InterruptedException {
ExecutorService service = Executors.newFixedThreadPool(100);
for (int i = 0; i < 100; i++) {
service.execute(() -> {
System.out.println(parse("2019-04-11 10:52:16"));
});
}
service.shutdown();
service.awaitTermination(1, TimeUnit.SECONDS);
}

也是《阿里巴巴开发手册》给出的解决方案,运行结果就不贴了,不会出现报错和时间不准确的问题,不过在运用中还是不习惯用DateTimeFormatter,或许下个项目中可以用上呢。