SimpleDateFormat不是线程安全的(thread safe)。这意味着,下面的代码在多线程环境下运行结果并非如我们所愿 - 有时候,它输出正确的日期,有时候会输出错误的(例如.Tue Aug 11 00:00:00 CST 48201),有些时候甚至会抛出NumberFormatException!!!(当然,在单线程环境是,这段代码是完全没有问题的)
打开JDK的源码,在format方法里,有这样一段代码:
calendar.setTime(date);
其中,calendar是DateFormat的protected字段。这条语句改变了calendar,稍后,calendar还会用到(在subFormat方法里),而这就是引发问题的根源。
想象一下,在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
- 线程1调用format方法,改变了calendar这个字段。
- 中断来了。
- 线程2开始执行,它也改变了calendar。
- 又中断了。
- 线程1回来了,此时,calendar已然不是它所设的值,而是走上了线程2设计的道路。
- BANG!!! 稍微花点时间分析一下format的实现,我们便不难发现,用到calendar,唯一的好处,就是在调用subFormat时,少了一个参数,却带来了这许多的问题。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解。
这个问题背后隐藏着一个更为重要的问题:无状态。
无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format方法在运行过程中改动了SimpleDateFormat的calendar字段,所以,它是有状态的。
所以,写程序,我们要尽量编写无状态方法。
我们如何在多线程中的使用DateFormat呢?
1. 同步
最简单的方法就是在做日期转换之前,为DateFormat对象加锁。这种方法使得一次只能让一个线程访问DateFormat对象,而其他线程只能等待。
public class DateFormatTest { private final DateFormat format = new SimpleDateFormat("yyyyMMdd"); public Date convert(String source) throws ParseException { synchronized(format) { return format.parse(source); } } }
2. 使用ThreadLocal
另外一个方法就是使用ThreadLocal变量去容纳DateFormat对象,也就是说每个线程都有一个属于自己的副本,并无需等待其他线程去释放它。这种方法会比使用同步块更高效。
public class DateFormatTest { private final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyyMMdd"); } }; public Date convert(String source) throws ParseException { return df.get().parse(source); } }
3. Joda-Time
Joda-Time 是一个很棒的开源的 JDK 的日期和日历 API 的替代品,其 DateTimeFormat 是线程安全而且不变的。
package com.uppower.test; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.util.Date; public class DateFormatTest { private final DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyyMMdd"); public Date convert(String source){ DateTime d = fmt.parseDateTime(source); returnd.toDate(); } }
4.使用临时变量(不推荐)
作为一个专业程序员,我们当然知道,相比于共享一个变量的开销要比每次创建小。创建一个实例来获取日期格式会比较高效,因为系统不需要多次获取本地语言和国家。
public class DateFormatTest { public Date convert(String source) throws ParseException { DateFormat format = new SimpleDateFormat("yyyyMMdd"); return format.parse(source); } }