天天看点

SimpleDateFormat 线程安全问题

SimpleDateFormat 线程安全问题

原创 猿星人 程序员开发者社区 2019-12-05

为啥线程不安全?

主要包含两大块 parse 和 format  不安全。

可以看到,多个线程之间共享变量calendar,并修改calendar。因此在多线程环境下,当多个线程同时使用相同的SimpleDateFormat对象(如static修饰)的话,如调用format方法时,多个线程会同时调用calender.setTime方法,导致time被别的线程修改,因此线程是不安全的。

SimpleDateFormat 的 format 方法线程不安全问题

    public final String format(Date arg0) {
        return this.format(arg0, new StringBuffer(),
                DontCareFieldPosition.INSTANCE).toString();
    }
           

this.format 使用的是 SimpleDateFormat 的format 方法

    private StringBuffer format(Date arg0, StringBuffer arg1, FieldDelegate arg2) {
        this.calendar.setTime(arg0);
        boolean arg3 = this.useDateFormatSymbols();
        int arg4 = 0;

        while (arg4 < this.compiledPattern.length) {
            int arg5 = this.compiledPattern[arg4] >>> 8;
            int arg6 = this.compiledPattern[arg4++] & 255;
            if (arg6 == 255) {
                arg6 = this.compiledPattern[arg4++] << 16;
                arg6 |= this.compiledPattern[arg4++];
            }

            switch (arg5) {
                case 100 :
                    arg1.append((char) arg6);
                    break;
                case 101 :
                    arg1.append(this.compiledPattern, arg4, arg6);
                    arg4 += arg6;
                    break;
                default :
                    this.subFormat(arg5, arg6, arg2, arg1, arg3);
            }
        }

        return arg1;
    }
           

不同线程

this.calendar.setTime(arg0);           

依然会导致线程不安全问题。

SimpleDateFormat是继承DateFormat类,DateFormat类中维护一个Calendar 对象

SimpleDateFormat 继承 DateFormat ,使用的calendar 是父类 DateFormat中的
public class SimpleDateFormat extends DateFormat {}

DateFormat 的 calendar 被用来进行 日期-时间计算,也被用于 format 方法也被用于 parse方法

public abstract class DateFormat extends Format {
    protected Calendar calendar;

}
           

Parse 导致的线程安全问题

SimpleDateFormat 的 parse 方法

public Date parse(String arg0, ParsePosition arg1) {
        this.checkNegativeNumberExpression();
        int arg2 = arg1.index;
        int arg3 = arg2;
        int arg4 = arg0.length();
        boolean[] arg5 = new boolean[]{false};
        CalendarBuilder arg6 = new CalendarBuilder();
        int arg7 = 0;

        label82 : while (arg7 < this.compiledPattern.length) {
            int arg8 = this.compiledPattern[arg7] >>> 8;
            int arg9 = this.compiledPattern[arg7++] & 255;
            if (arg9 == 255) {
                arg9 = this.compiledPattern[arg7++] << 16;
                arg9 |= this.compiledPattern[arg7++];
            }

            switch (arg8) {
                case 100 :
                    if (arg2 < arg4 && arg0.charAt(arg2) == (char) arg9) {
                        ++arg2;
                        break;
                    }

                    arg1.index = arg3;
                    arg1.errorIndex = arg2;
                    return null;
                case 101 :
                    while (true) {
                        if (arg9-- <= 0) {
                            continue label82;
                        }

                        if (arg2 >= arg4
                                || arg0.charAt(arg2) != this.compiledPattern[arg7++]) {
                            arg1.index = arg3;
                            arg1.errorIndex = arg2;
                            return null;
                        }

                        ++arg2;
                    }
                default :
                    boolean arg10 = false;
                    boolean arg11 = false;
                    if (arg7 < this.compiledPattern.length) {
                        int arg12 = this.compiledPattern[arg7] >>> 8;
                        if (arg12 != 100 && arg12 != 101) {
                            arg10 = true;
                        }

                        if (this.hasFollowingMinusSign
                                && (arg12 == 100 || arg12 == 101)) {
                            int arg13;
                            if (arg12 == 100) {
                                arg13 = this.compiledPattern[arg7] & 255;
                            } else {
                                arg13 = this.compiledPattern[arg7 + 1];
                            }

                            if (arg13 == this.minusSign) {
                                arg11 = true;
                            }
                        }
                    }

                    arg2 = this.subParse(arg0, arg2, arg8, arg9, arg10, arg5,
                            arg1, arg11, arg6);
                    if (arg2 < 0) {
                        arg1.index = arg3;
                        return null;
                    }
            }
        }

        arg1.index = arg2;

        try {
            Date arg15 = arg6.establish(this.calendar).getTime();
            if (arg5[0] && arg15.before(this.defaultCenturyStart)) {
                arg15 = arg6.addYear(100).establish(this.calendar).getTime();
            }

            return arg15;
        } catch (IllegalArgumentException arg14) {
            arg1.errorIndex = arg2;
            arg1.index = arg3;
            return null;
        }
    }
           

关键看

Date arg15 = arg6.establish(this.calendar).getTime();

这个里面 的 establish 方法。establish 是 CalendarBuilder 的方法

    Calendar establish(Calendar arg0) {
        boolean arg1 = this.isSet(17) && this.field[17] > this.field[1];
        if (arg1 && !arg0.isWeekDateSupported()) {
            if (!this.isSet(1)) {
                this.set(1, this.field[35]);
            }

            arg1 = false;
        }

        arg0.clear();

        int arg2;
        int arg3;
        for (arg2 = 2; arg2 < this.nextStamp; ++arg2) {
            for (arg3 = 0; arg3 <= this.maxFieldIndex; ++arg3) {
                if (this.field[arg3] == arg2) {
                    arg0.set(arg3, this.field[18 + arg3]);
                    break;
                }
            }
        }

        if (arg1) {
            arg2 = this.isSet(3) ? this.field[21] : 1;
            arg3 = this.isSet(7) ? this.field[25] : arg0.getFirstDayOfWeek();
            if (!isValidDayOfWeek(arg3) && arg0.isLenient()) {
                if (arg3 >= 8) {
                    --arg3;
                    arg2 += arg3 / 7;
                    arg3 = arg3 % 7 + 1;
                } else {
                    while (arg3 <= 0) {
                        arg3 += 7;
                        --arg2;
                    }
                }

                arg3 = toCalendarDayOfWeek(arg3);
            }

            arg0.setWeekDate(this.field[35], arg2, arg3);
        }

        return arg0;
    }
           

主要看

   arg0.clear();           

这个会将 calendar 清除掉,并且没有设置新值

可知SimpleDateFormat维护的用于format和parse方法计算日期-时间的calendar被清空了,如果此时线程A将calendar清空且没有来得及设置新值,线程B也进入parse方法用到了SimpleDateFormat对象中的calendar对象,此时就会产生线程安全问题!

SimpleDateFormat 线程安全问题

解决方案:

1、将SimpleDateFormat定义成局部变量

2、 加一把线程同步锁:synchronized(lock)

3、使用ThreadLocal,每个线程都拥有自己的SimpleDateFormat对象副本

解决办法栗子:threadLocal

class ThreadLocalSimpleFormatDateUtil {
    private static final String date_format = "yyyy-MM-dd HH:mm:ss";

    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>();

    public static DateFormat getDateFormat() {
        DateFormat df = threadLocal.get();
        if (df == null) {
            df = new SimpleDateFormat(date_format);
            threadLocal.set(df);
        }
        return df;
    }

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

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

测试代码

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

class MySimpleDateFormatThread extends Thread {
    private SimpleDateFormat sdf;
    private String dateString;
    public MySimpleDateFormatThread(SimpleDateFormat sdf, String dateString) {
        this.sdf = sdf;
        this.dateString = dateString;
    }

    @Override
    public void run() {
        try {
            Date date = sdf.parse(dateString);
            String dateStr = sdf.format(date);
            if(!dateStr.equals(dateString)) {
                System.out.println("ThreadName=" + this.getName() + "报错了,日期字符串:" + dateString + ",转换成的日期字符串:" + dateStr);
            } else {
                System.out.println("ThreadName=" + this.getName() + "成功,日期字符串:" + dateString);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

public class TestSimpleDateFormat {

    public static void main(String[] args) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        String[] dateString = {"2017-11-05","2017-11-06","2017-11-07","2017-11-08","2017-11-09","2017-11-10","2017-11-11","2017-11-12","2017-11-13","2017-11-14"};
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new MySimpleDateFormatThread(sdf, dateString[i]);
        }
        for (int i = 0; i < threads.length; i++) {
            threads[i].start();
        }
    }
}
           
ThreadName=Thread-3报错了,日期字符串:2017-11-08,转换成的日期字符串:2016-12-08
ThreadName=Thread-2报错了,日期字符串:2017-11-07,转换成的日期字符串:2016-12-08
ThreadName=Thread-7报错了,日期字符串:2017-11-12,转换成的日期字符串:2016-12-08
ThreadName=Thread-4报错了,日期字符串:2017-11-09,转换成的日期字符串:2200-11-09
ThreadName=Thread-8成功,日期字符串:2017-11-13java.lang.NumberFormatException: multiple points

ThreadName=Thread-9成功,日期字符串:2017-11-14
    at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
    at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
    at java.lang.Double.parseDouble(Unknown Source)
    at java.text.DigitList.getDouble(Unknown Source)
    at java.text.DecimalFormat.parse(Unknown Source)
    at java.text.SimpleDateFormat.subParse(Unknown Source)
    at java.text.SimpleDateFormat.parse(Unknown Source)
    at java.text.DateFormat.parse(Unknown Source)
    at JavaThread.MySimpleDateFormatThread.run(TestSimpleDateFormat.java:34)
java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source)
    at sun.misc.FloatingDecimal.parseDouble(Unknown Source)
    at java.lang.Double.parseDouble(Unknown Source)
    at java.text.DigitList.getDouble(Unknown Source)
    at java.text.DecimalFormat.parse(Unknown Source)
    at java.text.SimpleDateFormat.subParse(Unknown Source)
    at java.text.SimpleDateFormat.parse(Unknown Source)
    at java.text.DateFormat.parse(Unknown Source)
    at JavaThread.MySimpleDateFormatThread.run(TestSimpleDateFormat.java:34)
java.lang.NumberFormatException: For input string: ""
    at java.lang.NumberFormatException.forInputString(Unknown Source)
    at java.lang.Long.parseLong(Unknown Source)
    at java.lang.Long.parseLong(Unknown Source)
    at java.text.DigitList.getLong(Unknown Source)
    at java.text.DecimalFormat.parse(Unknown Source)
    at java.text.SimpleDateFormat.subParse(Unknown Source)
    at java.text.SimpleDateFormat.parse(Unknown Source)
    at java.text.DateFormat.parse(Unknown Source)
    at JavaThread.MySimpleDateFormatThread.run(TestSimpleDateFormat.java:34)
java.lang.ArrayIndexOutOfBoundsException: -1
    at java.text.DigitList.fitsIntoLong(Unknown Source)
    at java.text.DecimalFormat.parse(Unknown Source)
    at java.text.SimpleDateFormat.subParse(Unknown Source)
    at java.text.SimpleDateFormat.parse(Unknown Source)
    at java.text.DateFormat.parse(Unknown Source)
    at JavaThread.MySimpleDateFormatThread.run(TestSimpleDateFormat.java:34)