alex_lo

导航

SimpleDateFormat非线程安全

原文地址:

http://lilongfei1030.blog.163.com/blog/static/860152820136260822266/

 

 

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.jarjunit-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 来处理这个问题

posted on 2014-10-15 19:44  alex_lo  阅读(820)  评论(0编辑  收藏  举报