您的位置:首页 > 其它

SimpleDateFormat 使用注意事项

2017-07-27 11:33 507 查看
SimpleDateFormat类的继承关系:

java.text 

Class SimpleDateFormat

java.lang.Object

 |

   +----java.text.Format

 |

   +----java.text.DateFormat

|

   +----java.text.SimpleDateFormat

 

源文档 <http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>

 

该类用来对日期字符串进行解析和格式化输出.

 

SimpleDateFormatjavadoc中有这么句话:

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.

 

源文档 <http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>

 

翻译一下:

*日期格式是不同步的.

* 建议为每个线程创建独立的格式实例.

* 如果多线程并发访问同一个格式,则必须保持外部同步.

 

简而言之,SimpleDateFormat不是线程安全的,所以在多线程的环境下,往往会出现意想不到的结果.每次使用时,都创建一个新的SimpleDateFormat实例,或做加锁来同步使用.

 

SimpleDateFormat相关问题

(1)性能问题,主要是创建一个 SimpleDateFormat实例的开销比较昂贵,解析字符串时间时频繁创建生命周期短暂的实例导致性能低下。

代码如下:

public class DateUtil {

   

        private static final String FORMAT_YYYMMDD = "yyyy-MM-dd";

 

    public static String newFormatDate(Date date) {

        return new SimpleDateFormat(FORMAT_YYYMMDD).format(date);

    }

 

    public static Date newParse(String strDate) throws ParseException {

        return new SimpleDateFormat(FORMAT_YYYMMDD).parse(strDate);

    }

}

 

(2)并发非线程安全问题,即使将 SimpleDateFormat定义为静态类变量,貌似能解决这个问题,但是SimpleDateFormat是非线程安全的,可能引发并发非线程安全问题。

代码如下:

public class DateUtil {

     

      private static final SimpleDateFormat SIMPLE_DATE_FORMAT_OBJECT = new SimpleDateFormat("yyyy-MM-dd");

 

    public static String formatDate(Date date) {

        return SIMPLE_DATE_FORMAT_OBJECT.format(date);

    }

 

    public static Date parse(String strDate) throws ParseException {

        return SIMPLE_DATE_FORMAT_OBJECT.parse(strDate);

    }

}

 

测试类

import java.util.Date;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.Semaphore;

 

import junit.framework.TestCase;

 

import org.apache.log4j.Logger;

import org.junit.Test;

 

/**

 *

 *@Title:SimpleDateFormatTest

 *@Description:SimpleDateFormat类测试

 *@Author:lilongfei

 *@Since:2013-7-20

 *@Version:1.1.0

 */

public class SimpleDateFormatTest extends TestCase {

    // 线程数

    private static final int THREAD_NUM = 50;

    // 客户端数

    private static final int CLIENT_NUM = 100;

 

    private static int failCount = 0;

 

    private static final Logger loger = getDefaultLogger();

 

    @Override

    public void setUp() throws Exception {

        // TODO: 实现测试前的初始化工作

    }

 

    @Override

    public void tearDown() throws Exception {

        // 实现测试完成后的垃圾回收、测试结果统计等工作

        loger.info("访问数:" + CLIENT_NUM);

        loger.info("并发数:" + THREAD_NUM);

        loger.info("断言失败数:" + failCount);

    }

 

    @Test

    public void test() {

        // 得到一个可复用线程的线程池

        ExecutorService exec = Executors.newCachedThreadPool();

        // 信号量

        final Semaphore semp = new Semaphore(THREAD_NUM);

        for (int index = 0; index < CLIENT_NUM; index++) {

            final int no = index;

            Runnable run = new Runnable() {

                public void run() {

                    // 获取一个准入许可

                    try {

                        semp.acquire();

                        // doFormatTest(no);

                        doParseTest();

                        // 释放一个许可

                        semp.release();

                    } catch (Throwable e) {

                        e.printStackTrace();

                    }

                }

            };

            // 在线程池中执行一个任务

            exec.execute(run);

        }

        // 退出线程池

        exec.shutdown();

    }

   

    /**

     *

     *

     * @Description:

     */

    private void doParseTest() {

        try {

            DateUtil.parse("2013-07-25");

        } catch (Throwable e) {

            failCount++;

            e.printStackTrace();

        }

    }

   

    /**

     *

     * @param no

     * @Description:

           *    测试时需要修改一下日期,如:当前日期为2013-07-25,明天为2013-07-26

     */

    private void doFormatTest(int no) {

        try {

            if (no % 2 == 0) {

                String today = DateUtil.formatDate(new Date());

                assertTrue("ERROR TODAY IS:" + today, "2013-07-25".equals(today));

            } else {

                String tomorrow = DateUtil.formatDate(new Date(new Date().getTime() + 1000 * 60 * 60 * 24));

                assertTrue("ERROR TOMORROW IS:" + tomorrow, "2013-07-26".equals(tomorrow));

            }

        } catch (Throwable e) {

            loger.error(e);

            failCount++;

        }

    }

 

    private static Logger getDefaultLogger() {

        return Logger.getLogger("Businesslog");

    }

 



测试用例中使用到log4j,需要在工程根目录下,新建log4j.properties文件

其中具体内容为

log4j.rootLogger=DEBUG

#将逻辑层log记录到BusinessLog,allLog中

log4j.logger.Businesslog=DEBUG,A1

#A1--打印到屏幕上

log4j.appender.A1=org.apache.log4j.ConsoleAppender

log4j.appender.A1.layout=org.apache.log4j.PatternLayout

log4j.appender.A1.layout.ConversionPattern=[%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%m%n

引入第三方Jar包,log4j-1.2.15.jar、junit-4.5.jar

 

执行结果为:

java.lang.NumberFormatException: multiple points

at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1084)

at java.lang.Double.parseDouble(Double.java:482)

at java.text.DigitList.getDouble(DigitList.java:141)

at java.text.DecimalFormat.parse(DecimalFormat.java:1276)

at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1375)

at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1124)

at java.text.DateFormat.parse(DateFormat.java:333)

at com.DateUtil.parse(DateUtil.java:20)

at com.SimpleDateFormatTest.doParseTest(SimpleDateFormatTest.java:143)

at com.SimpleDateFormatTest.access$0(SimpleDateFormatTest.java:141)

at com.SimpleDateFormatTest$1.run(SimpleDateFormatTest.java:71)

at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:650)

at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:675)

at java.lang.Thread.run(Thread.java:595)

java.lang.NumberFormatException: For input string: ""

at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)

at java.lang.Long.parseLong(Long.java:424)

at java.lang.Long.parseLong(Long.java:461)

at java.text.DigitList.getLong(DigitList.java:167)

at java.text.DecimalFormat.parse(DecimalFormat.java:1271)

at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1692)

at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1124)

at java.text.DateFormat.parse(DateFormat.java:333)

at com.DateUtil.parse(DateUtil.java:20)

at com.SimpleDateFormatTest.doParseTest(SimpleDateFormatTest.java:143)

at com.SimpleDateFormatTest.access$0(SimpleDateFormatTest.java:141)

at com.SimpleDateFormatTest$1.run(SimpleDateFormatTest.java:71)

at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:650)

at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:675)

at java.lang.Thread.run(Thread.java:595)

 

如果用 synchronized 线程同步同样面临性能上的问题,同步将导致性能下降(线程之间序列化的获取SimpleDateFormat实例)。

 

使用Threadlocal解决此问题

以下转载至:源文档 <http://www.oschina.net/question/12_45856> 未做验证。

对于每个线程SimpleDateFormat不存在影响他们之间协作的状态,为每个线程创建一个SimpleDateFormat变量的拷贝或者叫做副本,代码如下:

import java.text.DateFormat;

import java.text.ParseException;

import java.text.SimpleDateFormat;

import java.util.Date;

 

/**

* 使用ThreadLocal以空间换时间解决SimpleDateFormat线程安全问题。

*

* @author

*

*/

public class DateUtil {

 

private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";

 

@SuppressWarnings("rawtypes")

private static ThreadLocal threadLocal = new ThreadLocal() {

protected synchronized Object initialValue() {

return new SimpleDateFormat(DATE_FORMAT);

}

};

 

public static DateFormat getDateFormat() {

return (DateFormat) threadLocal.get();

}

 

public static Date parse(String textDate) throws ParseException {

return getDateFormat().parse(textDate);

}

}

 

创建一个ThreadLocal类变量,这里创建时用了一个匿名类,覆盖了initialValue方法,主要作用是创建时初始化实例。也可以采用下面方式创建;

 

 

import java.text.DateFormat;

import java.text.SimpleDateFormat;

 

/**

* 使用ThreadLocal以空间换时间解决SimpleDateFormat线程安全问题。

*

* @author

*

*/

public class DateUtil {

 

private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";

 

// 第一次调用get将返回null

private static ThreadLocal threadLocal = new ThreadLocal();

 

// 获取线程的变量副本,如果不覆盖initialValue,第一次get返回null,故需要初始化一个SimpleDateFormat,并set到threadLocal中

public static DateFormat getDateFormat() {

DateFormat df = (DateFormat) threadLocal.get();

if (df == null) {

df = new SimpleDateFormat(DATE_FORMAT);

threadLocal.set(df);

}

return df;

}

 

}

 

我们看下我们覆盖的initialValue方法:

 

protected T initialValue() {

return null;//直接返回null

}

 

关于ThreadLocal相关知识可参考:源文档<http://blog.sina.com.cn/s/blog_871746680100yuir.html>

 

   总之,使用 SimpleDateFormat 应该关注以下几点:

确保不会在多线程状态下使用同一个 DateFormat 或者 SimpleDateFormat 实例

如果多线程情况下需要访问同一个实例,那么请用同步方法

可以使用 JODA 日期时间处理库来避免这些问题

可以使用 commons-lang 包中的
FastDateFormat 工具类

Commons项目中用来处理Java基本对象方法的工具类包,可以简化很多平时经常要用到的写法,例如判断字符串是否为空等等。

可以使用 ThreadLocal 来处理这个问题

下面我们通过看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();        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.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.我们的类和方法在做设计的时候,要尽量设计成无状态的

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: