深切懂得Java:SimpleDateFormat安然的时候格局化

    添加时间:2013-5-31 点击量:

      想必大师对SimpleDateFormat并不陌生。SimpleDateFormat 是 Java 中一个非经常用的类,该类用来对日期字符串进行解析和格局化输出,但若是应用不警惕会导致很是奥妙和难以调试的题目,因为 DateFormat 和 SimpleDateFormat 类不都是线程安然的,在多线程景象下调用 format() 和 parse() 办法应当应用同步代码来避免题目。下面我们经由过程一个具体的场景来一步步的深切进修和懂得SimpleDateFormat类。


      一.引子
      我们都是优良的法度员,我们都知道在法度中我们该当尽量少的创建SimpleDateFormat 实例,因为创建这么一个实例须要花费很大的价格。在一个读取数据库数据导出到excel文件的例子傍边,每次处理惩罚一个时候信息的时辰,就须要创建一个SimpleDateFormat实例对象,然后再丢弃这个对象。多量的对象就如许被创建出来,占用多量的内存和 jvm空间。代码如下:



    package com.peidasoft.dateformat;
    

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

    public class DateUtil {

    public static String formatDate(Date date)throws ParseException{
    SimpleDateFormat sdf
    = new SimpleDateFormat(yyyy-MM-dd HH:mm:ss);
    return sdf.format(date);
    }

    public static Date parse(String strDate) throws ParseException{
    SimpleDateFormat sdf
    = new SimpleDateFormat(yyyy-MM-dd HH:mm:ss);
    return sdf.parse(strDate);
    }
    }


      你也许会说,OK,那我就创建一个静态的simpleDateFormat实例,然后放到一个DateUtil类(如下)中,在应用时直接应用这个实例进行操纵,如许题目就解决了。改进后的代码如下:



    package com.peidasoft.dateformat;
    

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

    public class DateUtil {
    private static final SimpleDateFormat sdf = new SimpleDateFormat(yyyy-MM-dd HH:mm:ss);

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

    public static Date parse(String strDate) throws ParseException{

    return sdf.parse(strDate);
    }
    }


      当然,这个办法的确很不错,在大项目组的时候里面都邑工作得很好。但当你在临盆景象中应用一段时候之后,你就会发明这么一个事实:它不是线程安然的。在正常的测试景象之下,都没有题目,但一旦在临盆景象中必然负载景象下时,这个题目就出来了。他会呈现各类不合的景象,比如转化的时候不正确,比如报错,比如线程被挂死等等。我们看下面的测试用例,那事实措辞:



    package com.peidasoft.dateformat;
    

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

    public class DateUtil {

    private static final SimpleDateFormat sdf = new SimpleDateFormat(yyyy-MM-dd HH:mm:ss);

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

    public static Date parse(String strDate) throws ParseException{

    return sdf.parse(strDate);
    }
    }



    package com.peidasoft.dateformat;
    

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

    public class DateUtilTest {

    public static class TestSimpleDateFormatThreadSafe extends Thread {
    @Override
    public void run() {
    whiletrue) {
    try {
    this.join(2000);
    }
    catch (InterruptedException e1) {
    e1.printStackTrace();
    }
    try {
    System.out.println(
    this.getName()+:+DateUtil.parse(2013-05-24 06:02:20));
    }
    catch (ParseException e) {
    e.printStackTrace();
    }
    }
    }
    }


    public static void main(String[] args) {
    forint i = 0; i < 3; i++){
    new TestSimpleDateFormatThreadSafe().start();
    }

    }
    }


      履行输出如下:



    Exception in thread Thread-1 java.lang.NumberFormatException: multiple points
    
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:
    1082
    at java.lang.Double.parseDouble(Double.java:
    510
    at java.text.DigitList.getDouble(DigitList.java:
    151
    at java.text.DecimalFormat.parse(DecimalFormat.java:
    1302
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:
    1589
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:
    1311
    at java.text.DateFormat.parse(DateFormat.java:
    335
    at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:
    17
    at com.peidasoft.orm.dateformat.DateUtilTest¥TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:
    20
    Exception in thread
    Thread-0 java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:
    1082
    at java.lang.Double.parseDouble(Double.java:
    510
    at java.text.DigitList.getDouble(DigitList.java:
    151
    at java.text.DecimalFormat.parse(DecimalFormat.java:
    1302
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:
    1589
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:
    1311
    at java.text.DateFormat.parse(DateFormat.java:
    335
    at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:
    17
    at com.peidasoft.orm.dateformat.DateUtilTest¥TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:
    20
    Thread
    -2:Mon May 24 06:02:20 CST 2021
    Thread
    -2:Fri May 24 06:02:20 CST 2013
    Thread
    -2:Fri May 24 06:02:20 CST 2013
    Thread
    -2:Fri May 24 06:02:20 CST 2013


      申明:Thread-1和Thread-0报java.lang.NumberFormatException: multiple points错误,直接挂死,没起来;Thread-2 固然没有挂死,但输出的时候是有错误的,比如我们输入的时候是:2013-05-24 06:02:20 ,当会输出:Mon May 24 06:02:20 CST 2021 如许的灵异事务。


      二.原因


      作为一个专业法度员,我们当然都知道,比拟于共享一个变量的开销要比每次创建一个新变量要小很多。上方的优化过的静态的SimpleDateFormat版,之地点并发景象下回呈现各类灵异错误,是因为SimpleDateFormat和DateFormat类不是线程安然的。我们之所以忽视线程安然的题目,是因为从SimpleDateFormat和DateFormat类供给给我们的接口上来看,其实让人看不出它与线程安然有何相干。只是在JDK文档的最下面有如下申明:


      SimpleDateFormat中的日期格局不是同步的。推荐(建议)为每个线程创建自力的格局实例。若是多个线程同时接见一个格局,则它必须对峙外部同步。


      JDK原始文档如下:
      Synchronization:
      Date formats are not synchronized.
      It is recommended to create separate format instances for each thread.
      If multiple threads access a format concurrently, it must be synchronized externally.


      下面我们经由过程看JDK源码来看看为什么SimpleDateFormat和DateFormat类不是线程安然的真正原因:


      SimpleDateFormat持续了DateFormat,在DateFormat中定义了一个protected属性的 Calendar类的对象:calendar。只是因为Calendar累的概念错杂,牵扯到时区与本地化等等,Jdk的实现中应用了成员变量来传递参数,这就造成在多线程的时辰会呈现错误。


      在format办法里,有如许一段代码:



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

    boolean useDateFormatSymbols = useDateFormatSymbols();

    forint i = 0; i < compiledPattern.length; ) {
    int tag = compiledPattern[i] >>> 8;
    int count = compiledPattern[i++] & 0 xff;
    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.setTime(date)这条语句改变了calendar,稍后,calendar还会用到(在subFormat办法里),而这就是激发题目的根源。想象一下,在一个多线程景象下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format办法:
      线程1调用format办法,改变了calendar这个字段。
      中断来了。
      线程2开端履行,它也改变了calendar。
      又中断了。
      线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的路子。若是多个线程同时争抢calendar对象,则会呈现各类题目,时候不合错误,线程挂死等等。
      解析一下format的实现,我们不难发明,用到成员变量calendar,独一的益处,就是在调用subFormat时,少了一个参数,却带来了这很多的题目。其实,只要在这里用一个局部变量,一路传递下去,所有题目都将水到渠成。
      这个题目背后隐蔽着一个更为首要的题目--无状况:无状况办法的益处之一,就是它在各类景象下,都可以安然的调用。衡量一个办法是否是有状况的,就看它是否批改了其它的器材,比如全局变量,比如实例的字段。format办法在运行过程中批改了SimpleDateFormat的calendar字段,所以,它是有状况的。


      这也同时提示我们在开辟和设计体系的时辰重视下一下三点:


      1.本身写公用类的时辰,要对多线程调用景象下的结果在注释里进行明白申明


      2.对线程景象下,对每一个共享的可变变量都要重视其线程安然性


      3.我们的类和办法在做设计的时辰,要尽量设计成无状况的


      三.解决办法


      1.须要的时辰创建新实例:



    package com.peidasoft.dateformat;
    

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

    public class DateUtil {

    public static String formatDate(Date date)throws ParseException{
    SimpleDateFormat sdf
    = new SimpleDateFormat(yyyy-MM-dd HH:mm:ss);
    return sdf.format(date);
    }

    public static Date parse(String strDate) throws ParseException{
    SimpleDateFormat sdf
    = new SimpleDateFormat(yyyy-MM-dd HH:mm:ss);
    return sdf.parse(strDate);
    }
    }


      申明:在须要用到SimpleDateFormat 的处所新建一个实例,不管什么时辰,将有线程安然题目的对象由共享变为局部私有都能避免多线程题目,不过也加重了创建对象的肩负。在一般景象下,如许其实对机能影响比不是很明显的。


      2.应用同步:同步SimpleDateFormat对象



    package com.peidasoft.dateformat;
    

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

    public class DateSyncUtil {

    private static SimpleDateFormat sdf = new SimpleDateFormat(yyyy-MM-dd HH:mm:ss);

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

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


      申明:当线程较多时,当一个线程调用该办法时,其他想要调用此办法的线程就要block,多线程并发量大的时辰会对机能有必然的影响。


      3.应用ThreadLocal: 



    package com.peidasoft.dateformat;
    

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

    public class ConcurrentDateUtil {

    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 dateStr) throws ParseException {
    return threadLocal.get().parse(dateStr);
    }

    public static String format(Date date) {
    return threadLocal.get().format(date);
    }
    }


      别的一种写法:



    package com.peidasoft.dateformat;
    

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

    public class ThreadLocalDateUtil {
    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);
    }
    }


      申明:应用ThreadLocal, 也是将共享变量变为独享,线程独享必然能比办法独享在并发景象中能削减不少创建对象的开销。若是对机能请求斗劲高的景象下,一般推荐应用这种办法。


      4.扔掉JDK,应用其他类库中的时候格局化类:


      1.应用Apache commons 里的FastDateFormat,传播鼓吹是既快又线程安然的SimpleDateFormat, 可惜它只能对日期进行format, 不克不及对日期串进行解析。


      2.应用Joda-Time类库来处理惩罚时候相干题目


       


      做一个简单的压力测试,办法一最慢,办法三快,然则就算是最慢的办法一机能也不差,一般体系办法一和办法二就可以满足,所以说在这个点很难成为你体系的瓶颈地点。从简单的角度来说,建议应用办法一或者办法二,若是在须要的时辰,寻求那么一点机能提拔的话,可以推敲用办法三,用ThreadLocal做缓存。


      Joda-Time类库对时候处理惩罚体式格式斗劲完美,建议应用。


      参考材料:


      1.http://dreamhead.blogbus.com/logs/215637834.html


      2.http://www.blogjava.net/killme2008/archive/2011/07/10/354062.html

    所有随风而逝的都属于昨天的,所有历经风雨留下来的才是面向未来的。—— 玛格丽特·米切尔 《飘》
    分享到: