Dateformat在多线程下的异常问题

来源:互联网 发布:网络可能被劫持 编辑:程序博客网 时间:2024/06/16 11:37

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