为什么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
import java.text.DateFormat;
import java.text.SimpleDateFormat;

public class SimpleDateFormatNotThreadSafe {

private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");

public static void main (String[] args) {

for(int i = 1 ; i < 10; i++) {
Thread t = new MyThread(String.valueOf(i));
t.start();
}
}

static class MyThread extends Thread {

MyThread(String threadName) {
super(threadName);
}

@Override
public void run() {
try{
System.out.println("Created Thread " + getName() + ", "+ FORMAT.parse("2016-01-01"));
} catch( Exception e){
e.printStackTrace();
}
}
}

}

基于是多线程程序,线程运行的先后不可预知。但是奇怪的事情发生了,连可预知的FORMAT.parse("2016-01-01")也出现了奇怪的结果。

以下是几个可能出现的错误结果:

错误结果1

1
2
3
4
5
6
7
8
9
10
Created Thread 2, Wed Jan 01 00:00:00 HKT 2200
Created Thread 6, Fri Jan 01 00:00:00 HKT 2016
Created Thread 7, Fri Jan 01 00:00:00 HKT 2016
Created Thread 8, Fri Jan 01 00:00:00 HKT 2016
Created Thread 4, Fri Jan 01 00:00:00 HKT 2016
Created Thread 3, Fri Jan 01 00:00:00 HKT 2016
Created Thread 9, Fri Jan 01 00:00:00 HKT 2016
Created Thread 0, Wed Jan 01 00:00:00 HKT 2200
Created Thread 1, Fri Jan 01 00:00:00 HKT 2016
Created Thread 5, Fri Jan 01 00:00:00 HKT 2016

错误结果2

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
Created Thread 1, Sat May 26 09:34:08 HKT 164390675
Created Thread 3, Fri Jan 01 00:00:00 HKT 2016
Created Thread 4, Fri Jan 01 00:00:00 HKT 2016
Created Thread 0, Fri Jan 01 00:00:00 HKT 42016
Created Thread 5, Fri Jan 01 00:00:00 HKT 2016
Created Thread 6, Fri Jan 01 00:00:00 HKT 2016
Created Thread 2, Fri Jan 01 00:00:00 HKT 42016
Created Thread 7, Fri Jan 01 00:00:00 HKT 2016
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:2088)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)
at java.text.DateFormat.parse(DateFormat.java:355)
at com.examples.tutorials.SimpleDateFormatNotThreadSafe$MyThread.run(SimpleDateFormatNotThreadSafe.java:27)
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)
at com.examples.tutorials.SimpleDateFormatNotThreadSafe$MyThread.run(SimpleDateFormatNotThreadSafe.java:27)

错误结果3

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
java.lang.NumberFormatException: For input string: ".11E.111E"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1250)
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)
at com.examples.tutorials.SimpleDateFormatNotThreadSafe$MyThread.run(SimpleDateFormatNotThreadSafe.java:27)
java.lang.NumberFormatException: For input string: ".11E.111E1"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1250)
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)
at com.examples.tutorials.SimpleDateFormatNotThreadSafe$MyThread.run(SimpleDateFormatNotThreadSafe.java:27)
Created Thread 1, Fri Jan 01 00:00:00 HKT 2016
Created Thread 6, Fri Jan 01 00:00:00 HKT 2016
Created Thread 3, Fri Jan 01 00:00:00 HKT 2016
Created Thread 5, Fri Jan 01 00:00:00 HKT 2016
Created Thread 4, Fri Jan 01 00:00:00 HKT 2016
Created Thread 7, Fri Jan 01 00:00:00 HKT 2016
Created Thread 8, Fri Jan 01 00:00:00 HKT 2016
Created Thread 9, Fri Jan 01 00:00:00 HKT 2016

这正是因为SimpleDateFormat是非线程安全所导致的,这应该是设计者的错误。但是基于向后兼容,JDK中一直没有将它改为线程安全。然后Java 8引入了新的类解决了这一问题,下面会进行详细阐述。
从SimpleteDateFormat的源代码中可以知道,parse(..)的时候,首先会calender.clear(),然后会calender.set(..)。如果一个线程parse(..)还没有返回,另一个线程也进入了parse(..)并进行了calender.clear(),那么第一个线程将会得到意想不到的结果。

所以在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.

如何写出线程安全的date formatter

针对写出线程安全的date formatter,在Java 8之前主要有三种方法:

  • 每个线程使用单独的DateFormat实例
  • 使用synchronized进行同步
  • 使用ThreadLocal

每个线程使用单独的DateFormat实例

在每个线程里,创建一个新的DateFormat实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class SimpleDateFormatThreadSafe {

public Date convertStringToDate(String dateString) throws ParseException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
return df.parse(dateString);
}

}

这种方法虽然实现起来很简单,但是效率非常的低。

使用synchronized进行同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class SimpleDateFormatThreadSafeSynchronized {

private static final DateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd");

public Date convertStringToDate(String dateString) throws ParseException {
Date result;
synchronized(FORMAT) {
result = FORMAT.parse(dateString);
}
return result;
}

}

使用ThreadLocal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.text.DateFormat;
import java.text.SimpleDateFormat;

public class ThreadLocalThreadSafe {

private static final ThreadLocal<DateFormat> FORMAT = new ThreadLocal<DateFormat>(){

@Override
protected DateFormat initialValue(){
return new SimpleDateFormat("yyyy-MM-dd");
}
};

public Date convertStringToDate(String dateString) throws ParseException {
return FORMAT.get().parse(dateString);
}

}

以上三种方法的性能测试

有人针对以上的三种方法进行了性能测试,性能排序为:

使用ThreadLocal > 使用synchronized进行同步 > 每个线程使用单独的DateFormat实例

但是需要指出的是ThreadLocal的性能是在有线程池的情况下,假如没有线程池,和每个线程单独new一个DateFormat实例是没有区别的

性能排序

所以我建议,多线程环境下,最好不用使用SimpleDateFormat,无论哪种方法都会写一些多余的代码,性能也不见得好。

线程安全的Date Formatter

在Java 8之前的Java版本,我们可以考虑使用线程安全的类

Java 8中的DateTimeFormatter

Java 8中引入了新的包java.time,完美的解决了线程不安全的问题。

将字符串转换成java.time.LocalDate:

1
2
3
4
5
6
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");

//convert String to LocalDate
LocalDate localDate = LocalDate.parse("2016-01-01", formatter);

System.out.println(localDate);