想必大家对SimpleDateFormat并不陌生。不过,你是否知道,SimpleDateFormat不是线程安全的(thread safe)。这意味着,下面的代码在多线程环境下运行结果并非如我们所愿 - 有时候,它输出正确的日期,有时候会输出错误的(例如.Tue Aug 11 00:00:00 CST 48201),有些时候甚至会抛出NumberFormatException!!!(当然,在单线程环境是,这段代码是完全没有问题的)
package com.uppower.test; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateFormatTest { private final DateFormat format = new SimpleDateFormat("yyyyMMdd"); public Date convert(String source) throws ParseException { return format.parse(source); } }
package com.uppower.test; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; public class Test { public static void main(String[] args) throws Exception { final DateFormatTest t =new DateFormatTest(); Callable<Date> task =new Callable<Date>(){ public Date call()throws Exception { return t.convert("20100811"); } }; //尝试5个线程的情况 ExecutorService exec = Executors.newFixedThreadPool(5); List<Future<Date>> results = new ArrayList<Future<Date>>(); //实现100次日期转换 for(int i =0; i <100; i++){ results.add(exec.submit(task)); } exec.shutdown(); //查看结果 for(Future<Date> result : results){ System.out.println(result.get()); } } }
运行结果:
Wed Aug 11 00:00:00 CST 2010
Tue Aug 11 00:00:00 CST 48201
Wed Aug 11 00:00:00 CST 2010
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.NumberFormatException: For input string: "E.1118E1"
at java.util.concurrent.FutureTask$Sync.innerGet(FutureTask.java:222)
at java.util.concurrent.FutureTask.get(FutureTask.java:83)
at com.uppower.test.DateFormatTest.main(DateFormatTest.java:44)
Caused by: java.lang.NumberFormatException: For input string: "E.1118E1"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1224)
at java.lang.Double.parseDouble(Double.java:510)
at java.text.DigitList.getDouble(DigitList.java:151)
at java.text.DecimalFormat.parse(DecimalFormat.java:1303)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1312)
at java.text.DateFormat.parse(DateFormat.java:335)
at com.uppower.test.DateFormatTest.convert(DateFormatTest.java:20)
at com.uppower.test.DateFormatTest$1.call(DateFormatTest.java:28)
at com.uppower.test.DateFormatTest$1.call(DateFormatTest.java:1)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:303)
at java.util.concurrent.FutureTask.run(FutureTask.java:138)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:619)
接下来,我们看看为什么DateFormat不是线程安全的。
我们就要打开JDK的源码,在format方法里,有这样一段代码:
calendar.setTime(date);
其中,calendar是DateFormat的protected字段。这条语句改变了calendar,稍后,calendar还会用到(在subFormat方法里),而这就是引发问题的根源。
想象一下,在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat的实例,分别调用format方法:
这个问题背后隐藏着一个更为重要的问题:无状态。
无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format方法在运行过程中改动了SimpleDateFormat的calendar字段,所以,它是有状态的。
所以,写程序,我们要尽量编写无状态方法。
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); } }
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(); } }
public class DateFormatTest { public Date convert(String source) throws ParseException { DateFormat format = new SimpleDateFormat("yyyyMMdd"); return format.parse(source); } }