SimpleDateFormat非线程安全
原文地址:
http://lilongfei1030.blog.163.com/blog/static/860152820136260822266/
SimpleDateFormat类的继承关系:
java.text
Class SimpleDateFormat
|
+----java.text.Format
|
+----java.text.DateFormat
|
+----java.text.SimpleDateFormat
源文档 <http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>
该类用来对日期字符串进行解析和格式化输出.
SimpleDateFormat的javadoc中有这么句话:
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 来处理这个问题