Dateformat在多线程下的异常问题

SimpleDateFormat是我们经常使用的一个对日期字符串进行解析和格式化输出的类,但是使用不当会带来意想不到的问题。因为SimpleDateFormat中format()和parse()方法是线程不安全的,所以在多线程调用时会出现异常、转换不正确等问题,下面,我们来分析一下,

一般情况下,我们转换日期较多就会写一个通用Utils类来做时间转换与格式化,如下

class DateUtil{


    public static Date parse(String str) throws ParseException {
    SimpleDateFormat sim = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
            return sim.parse(str);
    }
    public static String format(Date date){
       SimpleDateFormat sim = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
      return sim.format(date);

    }
}

但作为一名优秀程序员,我们才不允许在jvm中频繁创建SimpleDateFormat对象,所以我们会写成这样:

class DateUtil{
    public static SimpleDateFormat sim = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public static Date parse(String str) throws ParseException {
            return sim.parse(str);
    }
    public static String format(Date date){
            return sim.format(date);
    }
}

下面我们来测试一下:

public class TestDateFormat {
    public static void main(String[] args) {
        for(int i=0; i<10000 ;i++){
            String format = DateUtil.format(new Date());

            new Thread(){
                @Override
                public void run() {
                    while (true){
                        try {
                            this.join(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        String format = DateUtil.format(new Date());
                        try {
                            DateUtil.parse(format);

                        } catch (ParseException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }.start();
        }
    }
}

运行起来之后,其结果如下:

Exception in thread "Thread-7096" java.lang.NumberFormatException: For input string: ""
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Long.parseLong(Long.java:601)
    at java.lang.Long.parseLong(Long.java:631)
    at java.text.DigitList.getLong(DigitList.java:195)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at Javabase.DateUtil.parse(TestDateFormat.java:43)
    at Javabase.TestDateFormat$1.run(TestDateFormat.java:28)

那么问题来了 ,为什么会这样呢?
通过分析JDK源码,就会发现,jdk中有这么一段:

protected Calendar calendar;
......
private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date);

        boolean useDateFormatSymbols = useDateFormatSymbols();

        for (int i = 0; i < compiledPattern.length; ) {
            int tag = compiledPattern[i] >>> 8;
            int count = compiledPattern[i++] & 0xff;
            if (count == 255) {
                count = compiledPattern[i++] << 16;
                count |= compiledPattern[i++];
            }

            switch (tag) {
            case TAG_QUOTE_ASCII_CHAR:
                toAppendTo.append((char)count);
                break;

            case TAG_QUOTE_CHARS:
                toAppendTo.append(compiledPattern, i, count);
                i += count;
                break;

            default:
                subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
                break;
            }
        }
        return toAppendTo;
    }

这里的calendar对象会作为全局变量在format()中调用,而且会改变它的值,着我们就能想象得到:在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
  线程1调用format方法,改变了calendar这个字段。
  中断来了。
  线程2开始执行,它也改变了calendar。
  又中断了。
  线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。如果多个线程同时争抢calendar对象,则会出现各种问题,时间不对,线程挂死等等。

那么,怎么解决这个问题呢,这里提供一些建议:
1、就是最开始那样:

class DateUtil{

    public static Date parse(String str) throws ParseException {
        SimpleDateFormat sim = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
            return sim.parse(str);
    }
    public static String format(Date date){
        SimpleDateFormat sim = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
            return sim.format(date);
    }
}

这样无疑不会出现问题,但是每次都得创建新对象;
2、给SimpleDateFormat 加上snychroniaed:

class DateUtil{
    public static SimpleDateFormat sim = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public static Date parse(String str) throws ParseException {
        synchronized(sim){
            return sim.parse(str);
        }
    }
    public static String format(Date date){
        synchronized(sim){
            return sim.format(date);
        }
    }
}

这就会比上面那个少创建对象,每次调用时都会检查SimpleDateFormat 对象是否在使用,故就不会出现同事调用的问题,而且性能比第一个好;
3、用TheardLocal<>修饰SimpleDateFormat :

class DateUtil{
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static Date parse(String str) throws ParseException {
        return threadLocal.get().parse(str);
    }
    public static String format(Date date){
        return threadLocal.get().format(date);
    }
}

线程共享,就是在多线程下每个线程调用本线程内的SimpleDateFormat ,就不会出现上面问题。因为这个方式会在每个线程中都有一个SimpleDateFormat 实例,所以性能会比1和2都好,就是对象多了一点;

总结:
如果不要求性能就是用方法一,如果性能要求不大,就可以使用方法二,如果对想能要求很高,方法三可以解决很大问题
另外,还可以使用第三方来解决这个问题,Apache commons 里的FastDateFormat,宣称是既快又线程安全的SimpleDateFormat, 可惜它只能对日期进行format, 不能对日期串进行解析

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值