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);
}
}
测试类
imp
imp
imp
imp
imp
imp
imp
/**
*
*@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变量的拷贝或者叫做副本,代码如下:
imp
imp
imp
imp
/**
* 使用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方法,主要作用是创建时初始化实例。也可以采用下面方式创建;
imp
imp
/**
* 使用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.我们的类和方法在做设计的时候,要尽量设计成无状态的