SimpleDateFormat的常用方法
SimpleDateFormat相信大家都不陌生,主要是用来对日期进行转换
日期->字符串 format(date)
字符串->日期 parse(dateStr)
由于是一个日期的格式转换,所以一般都会作为公共资源供大家一起使用,如DateUtils:
public class DateUtils {
private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String format(Date date){
return sf.format(date);
}
public static Date parse(String dateStr){
return sf.parse(dateStr);
}
}
问题复现
但SimpleDateFormat是非线程安全的,如果作为工具类,定义为static类型的,在多线程场景下就会存在问题,我们通过代码验证一下:
public class DateFormatTest implements Runnable{
private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public void run() {
try {
System.out.println(Thread.currentThread().getName() + " : " + sf.parse("2019-11-07 15:40:19"));
} catch (ParseException e) {
e.printStackTrace();
}
}
public static void main(String args[]){
DateFormatTest dt = new DateFormatTest();
for(int i=0; i<10; i++){
new Thread(dt).start();
}
}
}
跑了10个线程,结果:
Exception in thread "Thread-7" Exception in thread "Thread-3" Exception in thread "Thread-0" java.lang.NumberFormatException: For input string: ""
at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.base/java.lang.Long.parseLong(Long.java:702)
at java.base/java.lang.Long.parseLong(Long.java:817)
at java.base/java.text.DigitList.getLong(DigitList.java:195)
at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2123)
at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2240)
at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1541)
at java.base/java.text.DateFormat.parse(DateFormat.java:393)
at user.DateFormatTest.run(DateFormatTest.java:16)
at java.base/java.lang.Thread.run(Thread.java:834)
Exception in thread "Thread-2" java.lang.NumberFormatException: For input string: ".22200199E4.2019E4"
at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054)
at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.base/java.lang.Double.parseDouble(Double.java:543)
at java.base/java.text.DigitList.getDouble(DigitList.java:169)
at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2128)
at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1933)
at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1541)
at java.base/java.text.DateFormat.parse(DateFormat.java:393)
at user.DateFormatTest.run(DateFormatTest.java:16)
at java.base/java.lang.Thread.run(Thread.java:834)
java.lang.NumberFormatException: For input string: "E.2115E2"
at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054)
at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.base/java.lang.Double.parseDouble(Double.java:543)
at java.base/java.text.DigitList.getDouble(DigitList.java:169)
at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2128)
at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2240)
at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1541)
at java.base/java.text.DateFormat.parse(DateFormat.java:393)
at user.DateFormatTest.run(DateFormatTest.java:16)
at java.base/java.lang.Thread.run(Thread.java:834)
java.lang.NumberFormatException: For input string: "E.2115"
at java.base/jdk.internal.math.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2054)
at java.base/jdk.internal.math.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.base/java.lang.Double.parseDouble(Double.java:543)
at java.base/java.text.DigitList.getDouble(DigitList.java:169)
at java.base/java.text.DecimalFormat.parse(DecimalFormat.java:2128)
at java.base/java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2240)
at java.base/java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1541)
at java.base/java.text.DateFormat.parse(DateFormat.java:393)
at user.DateFormatTest.run(DateFormatTest.java:16)
at java.base/java.lang.Thread.run(Thread.java:834)
Thread-1 : Tue Nov 07 15:40:19 CST 2220
Thread-6 : Thu Nov 07 15:40:19 CST 2019
Thread-9 : Sun Nov 07 15:40:19 CST 1
Thread-8 : Thu Nov 07 15:40:19 CST 2019
Thread-4 : Thu Oct 31 15:40:19 CST 2019
Thread-5 : Thu Oct 31 15:40:19 CST 2019
有4个线程直接报错了,另外6个虽然没报错,大部分结果也不正确
为什么是非线程安全的
先来看一下SimpleDateFormat的源代码

calb是CalendarBuilder的实例,重新构建了calendar
pos.index = start;
Date parsedDate;
try {
// calb是CalendarBuilder的实例,用来构建
parsedDate = calb.establish(calendar).getTime();
// If the year value is ambiguous,
// then the two-digit year == the default start year
if (ambiguousYear[0]) {
if (parsedDate.before(defaultCenturyStart)) {
parsedDate = calb.addYear(100).establish(calendar).getTime();
}
}
}
看一下calb.establish()方法做了什么
Calendar establish(Calendar cal) {
boolean weekDate = isSet(WEEK_YEAR)
&& field[WEEK_YEAR] > field[YEAR];
if (weekDate && !cal.isWeekDateSupported()) {
// Use YEAR instead
if (!isSet(YEAR)) {
set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
}
weekDate = false;
}
// 清除了cal,后边又重新设值
cal.clear();
// Set the fields from the min stamp to the max stamp so that
// the field resolution works in the Calendar.
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;
}
}
}
}
结论
整体来看,SimpleDateFormat内部持有了Calendar calendar;这个成员变量,Calendar本身是非线程安全的,所以在多线程下发生了对共享资源的写操作,所以会引起线程安全问题
该如何使用SimpleDateFormat
- 使用局部变量
- 加锁同步
- 一个线程分配一个SimpleDateFormat,使用ThreadLocal