为什么SimpleDateFormat是非线程安全的

1,193 阅读2分钟

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

  1. 使用局部变量
  2. 加锁同步
  3. 一个线程分配一个SimpleDateFormat,使用ThreadLocal