前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SimpleDateFormat 线程安全问题

SimpleDateFormat 线程安全问题

作者头像
王小明_HIT
发布2019-12-10 17:30:44
8870
发布2019-12-10 17:30:44
举报
文章被收录于专栏:程序员奇点

为啥线程不安全?

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

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

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

代码语言:javascript
复制
    public final String format(Date arg0) {
        return this.format(arg0, new StringBuffer(),
                DontCareFieldPosition.INSTANCE).toString();
    }

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

代码语言:javascript
复制
    private StringBuffer format(Date arg0, StringBuffer arg1, FieldDelegate arg2) {
        this.calendar.setTime(arg0);
        boolean arg3 = this.useDateFormatSymbols();
        int arg4 = ;

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

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

        return arg1;
    }

不同线程

代码语言:javascript
复制
this.calendar.setTime(arg0);

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

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

代码语言:javascript
复制
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 方法

代码语言:javascript
复制
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 = ;

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

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

                    arg1.index = arg3;
                    arg1.errorIndex = arg2;
                    return null;
                case  :
                    while (true) {
                        if (arg9-- <= ) {
                            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] >>> ;
                        if (arg12 !=  && arg12 != ) {
                            arg10 = true;
                        }

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

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

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

        arg1.index = arg2;

        try {
            Date arg15 = arg6.establish(this.calendar).getTime();
            if (arg5[] && arg15.before(this.defaultCenturyStart)) {
                arg15 = arg6.addYear().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 的方法

代码语言:javascript
复制
    Calendar establish(Calendar arg0) {
        boolean arg1 = this.isSet() && this.field[] > this.field[];
        if (arg1 && !arg0.isWeekDateSupported()) {
            if (!this.isSet()) {
                this.set(, this.field[]);
            }

            arg1 = false;
        }

        arg0.clear();

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

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

                arg3 = toCalendarDayOfWeek(arg3);
            }

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

        return arg0;
    }

主要看

代码语言:javascript
复制
   arg0.clear();

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

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

解决方案:

1、将SimpleDateFormat定义成局部变量

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

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

解决办法栗子:threadLocal

代码语言:javascript
复制
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);
    }
}

关注公众号:程序员开发者社区

测试代码

代码语言:javascript
复制
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[];
        for (int i = ; i < threads.length; i++) {
            threads[i] = new MySimpleDateFormatThread(sdf, dateString[i]);
        }
        for (int i = ; i < threads.length; i++) {
            threads[i].start();
        }
    }
}

会有如下报错:

代码语言:javascript
复制
ThreadName=Thread-报错了,日期字符串:--,转换成的日期字符串:--
ThreadName=Thread-报错了,日期字符串:--,转换成的日期字符串:--
ThreadName=Thread-报错了,日期字符串:--,转换成的日期字符串:--
ThreadName=Thread-报错了,日期字符串:--,转换成的日期字符串:--
ThreadName=Thread-成功,日期字符串:--java.lang.NumberFormatException: multiple points

ThreadName=Thread-成功,日期字符串:--
    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:)
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:)
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:)
java.lang.ArrayIndexOutOfBoundsException: -
    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:)
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-12-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员奇点 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档