SimpleDateFormat 的问题

在 Java 中,格式化日期通常使用 SimpleDateFormat 这个类(JDK提供的)。

我们知道,SimpleDateFormat 是线程不安全的,SimpleDateFormat在对时间进行格式化的方法format中,会先对calendar对象进行setTime的赋值,若是有多个线程同时操作一个SimpleDateFormat实例的话,就会对calendar的赋值进行覆盖,进而产生问题。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final String format(Date date) {
return format(date, new StringBuffer(), DontCareFieldPosition.INSTANCE).toString();
}

// Called from Format after creating a FieldDelegate
private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date);
switch (tag) {
...
default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
...
}

calendar.setTime(date)这条语句改变了calendar,稍后,calendar还会用到(在subFormat方法里),而这就是引发问题的根源。想象一下,在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:

  1. 线程1调用format方法,改变了calendar这个字段。
  2. 中断来了。
  3. 线程2开始执行,它也改变了calendar。
  4. 又中断了。
  5. 线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。如果多个线程同时争抢calendar对象,则会出现各种问题,时间不对,线程挂死等等。

​ 分析一下format的实现,我们不难发现,用到成员变量calendar,唯一的好处,就是在调用subFormat时,少了一个参数,却带来了这许多的问题。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解。
  这个问题背后隐藏着一个更为重要的问题–无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format方法在运行过程中改动了SimpleDateFormat的calendar字段,所以,它是有状态的

所以,大部分时候则是在方法内部 new 出新的 DateFormat 对象再做格式化,如:DateFormat df = new SimpleDateFormat("yyyy-MM-dd");。但在高访问量的情况下,频繁创建实例也会导致内存开销大和 GC 频繁问题。

FastDateFormat

Apache 的 commons-lang 包下有个 FastDateFormat 可以方便的解决上述问题。

查看其源码:

1
2
3
4
5
6
7
8
9
10
11
/*FastDatePrinter#format
可以看到底层用的是Calendar
因为是在方法体内定义了了Calendar,而方法体内的局部变量是属于栈的
而栈是线程独享的,不存在线程安全问题
而SimpleDateFormat 用到的Calendar是成员变量,可以被多个线程共享,所以存在线程安全问题
*/
public String format(Date date) {
Calendar c = new GregorianCalendar(mTimeZone, mLocale);
c.setTime(date);
return applyRules(c, new StringBuffer(mMaxLengthEstimate)).toString();
}

在新的版本(3.0 以上)中 getInstance 方法内更加使用 ConcurrentMap 做缓存提高并发性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 3.0 版本源码
abstract class FormatCache<F extends Format> {
static final int NONE = -1;
private final ConcurrentMap<MultipartKey, F> cInstanceCache;
private final ConcurrentMap<MultipartKey, String> cDateTimeInstanceCache;
...
}

private static final FormatCache<FastDateFormat> cache = new FormatCache() {
protected FastDateFormat createInstance(String pattern, TimeZone timeZone, Locale locale) {
return new FastDateFormat(pattern, timeZone, locale);
}
};

public static FastDateFormat getInstance(String pattern, TimeZone timeZone, Locale locale) {
return (FastDateFormat)cache.getInstance(pattern, timeZone, locale);
}

DateFormatUtils

基于上面 FastDateFormat 类,commons-lang 包还提供了 DateFormatUtils 工具类,提供了两方面的功能:

  1. 提供静态的 format 方法,实现一行代码格式化日期。
  2. 内部初始化了一些 final 的 FastDateFormat,方便开发者直接调用。