2019-09-01 更新:

从 JDK 8 开始,本文描述的问题已经不是问题了。因为 JDK 8 带来了全新的 Date Time API。大体上来说,这套 API 吸收了 Joda Time 的优点,相当清晰、简单且灵活。同时,所有的时间表示都是不可变类,因此也天然是线程安全的。当然,SimpleDateFormat 的问题也有了更优雅的工具替代。

对时间的格式化和解析的用法对比如下:

1
2
3
4
5
// Old
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
Date now = new Date();
String formattedDate = dateFormat.format(now);
Date parsedDate = dateFormat.parse(formattedDate);
1
2
3
4
5
// New
LocalDate now = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String formattedDate = now.format(formatter);
LocalDate parsedDate = LocalDate.parse(formattedDate, formatter);

原文:

JDK 中的 SimpleDateFormat 是线程非安全的, 因此当在并发访问的情形下使用时一定要当心。尽管 Javadoc API 文档中明确指出了该类是未同步的,可能很多人在遇到了其导致的问题之前都不会注意到这点。

先看个例子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package gan.blog.example.sdf;

import java.text.ParseException;
import java.text.SimpleDateFormat;

/**
 * creates and starts 10 threads, each of which parses a same time stamp using
 * shared SimpleDateFormat
 */
public class SimpleDateFormatTester {

  private static SimpleDateFormat sdf = new SimpleDateFormat(
      "yyyy-MM-dd HH:mm:ss.SSS Z");

  public static void main(String[] args) {

    for (int i = 0; i < 5; i++) {
      Thread t = new Thread() {
        public void run() {
          try {
            System.out.println(sdf.parse("2014-06-27 10:33:09.258 +0800"));
          } catch (ParseException e) {
            e.printStackTrace();
          }
        }
      };

      t.start();
    }
  }
}

上述示例程序启动了五个线程,各线程都使用共享的 SimpleDateFormat 用来解析时间戳字符串。多运行几次上面的程序,很可能地你可能会看到下述结果或异常信息中的一种:

  1. Exception in thread “Thread-4” java.lang.NumberFormatException: For input string: “”
  2. Exception in thread “Thread-1” java.lang.NumberFormatException: multiple points
  3. Thu Jun 27 10:33:09 CST 2024
  4. Exception in thread “Thread-2” java.lang.NumberFormatException: empty String
  5. Fri Jun 27 10:33:09 CST 2200
  6. ……

如上所示,有时得到的是异常(大部分情况是 NumberFormatException: For input string: ""),有时给出的是错误的结果。

为了避免以上问题,可以在每次使用其解析或格式化字符串时都创建一个新的 SimpleDateFormat 实例。显然,这种处理方式在并发量大时并不是一个很节约的方式。一种更优化的方式是使用 ThreadLocal 变量存放,使得每个线程都独立的有一份 SimpleDateFormat, 通常来说你还可以将创建的实例进行缓存。该方案的实现细节请参考这篇 Jesper’s blog post, 文中还有对各解决方案性能的对比。