SimpleDateFormat是我们经常使用的一个对日期字符串进行解析和格式化输出的类,但是使用不当会带来意想不到的问题。因为SimpleDateFormat中format()和parse()方法是线程不安全的,所以在多线程调用时会出现异常、转换不正确等问题,下面,我们来分析一下,
一般情况下,我们转换日期较多就会写一个通用Utils类来做时间转换与格式化,如下
class DateUtil{
public static Date parse(String str) throws ParseException {
SimpleDateFormat sim = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return sim.parse(str);
}
public static String format(Date date){
SimpleDateFormat sim = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return sim.format(date);
}
}
但作为一名优秀程序员,我们才不允许在jvm中频繁创建SimpleDateFormat对象,所以我们会写成这样:
class DateUtil{
public static SimpleDateFormat sim = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static Date parse(String str) throws ParseException {
return sim.parse(str);
}
public static String format(Date date){
return sim.format(date);
}
}
下面我们来测试一下:
public class TestDateFormat {
public static void main(String[] args) {
for(int i=0; i<10000 ;i++){
String format = DateUtil.format(new Date());
new Thread(){
@Override
public void run() {
while (true){
try {
this.join(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String format = DateUtil.format(new Date());
try {
DateUtil.parse(format);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
}.start();
}
}
}
运行起来之后,其结果如下:
Exception in thread "Thread-7096" java.lang.NumberFormatException: For input string: ""
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:601)
at java.lang.Long.parseLong(Long.java:631)
at java.text.DigitList.getLong(DigitList.java:195)
at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at Javabase.DateUtil.parse(TestDateFormat.java:43)
at Javabase.TestDateFormat$1.run(TestDateFormat.java:28)
那么问题来了 ,为什么会这样呢?
通过分析JDK源码,就会发现,jdk中有这么一段:
protected Calendar calendar;
......
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对象会作为全局变量在format()中调用,而且会改变它的值,着我们就能想象得到:在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
线程1调用format方法,改变了calendar这个字段。
中断来了。
线程2开始执行,它也改变了calendar。
又中断了。
线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。如果多个线程同时争抢calendar对象,则会出现各种问题,时间不对,线程挂死等等。
那么,怎么解决这个问题呢,这里提供一些建议:
1、就是最开始那样:
class DateUtil{
public static Date parse(String str) throws ParseException {
SimpleDateFormat sim = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return sim.parse(str);
}
public static String format(Date date){
SimpleDateFormat sim = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return sim.format(date);
}
}
这样无疑不会出现问题,但是每次都得创建新对象;
2、给SimpleDateFormat 加上snychroniaed:
class DateUtil{
public static SimpleDateFormat sim = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static Date parse(String str) throws ParseException {
synchronized(sim){
return sim.parse(str);
}
}
public static String format(Date date){
synchronized(sim){
return sim.format(date);
}
}
}
这就会比上面那个少创建对象,每次调用时都会检查SimpleDateFormat 对象是否在使用,故就不会出现同事调用的问题,而且性能比第一个好;
3、用TheardLocal<>修饰SimpleDateFormat :
class DateUtil{
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 str) throws ParseException {
return threadLocal.get().parse(str);
}
public static String format(Date date){
return threadLocal.get().format(date);
}
}
线程共享,就是在多线程下每个线程调用本线程内的SimpleDateFormat ,就不会出现上面问题。因为这个方式会在每个线程中都有一个SimpleDateFormat 实例,所以性能会比1和2都好,就是对象多了一点;
总结:
如果不要求性能就是用方法一,如果性能要求不大,就可以使用方法二,如果对想能要求很高,方法三可以解决很大问题
另外,还可以使用第三方来解决这个问题,Apache commons 里的FastDateFormat,宣称是既快又线程安全的SimpleDateFormat, 可惜它只能对日期进行format, 不能对日期串进行解析