SimpleDateFormat 的多线程安全问题。这是生产环境中一个非常经典且危险的问题,因为它可能不会立即导致程序崩溃,而是 silently(静默地)产生错误的数据,极难排查。
问题根源:可变状态与竞争条件
SimpleDateFormat 不是线程安全的,其根本原因在于它内部维护了可变的、共享的状态(一个 Calendar 对象),并且没有使用同步机制来保护这个状态。
- 内部状态:当你调用
parse或format方法时,SimpleDateFormat会使用其内部的Calendar对象来执行计算。 - 竞争条件:
- 线程 A 调用
parse("2023-10-25"),开始解析,将日期值设置到内部的Calendar中。 - 在 线程 A 完成解析并返回结果之前,线程 B 也调用了
parse("2024-11-30"),并清空/覆盖了内部Calendar的状态。 - 此时,线程 A 继续执行,从被 线程 B 污染了的
Calendar中读取值,最终返回一个错误的Date对象(可能是 “2024-10-25” 或其他混乱的结果)。 - 更糟的情况下,可能会直接抛出
NumberFormatException、ArrayIndexOutOfBoundsException等异常。
- 线程 A 调用
问题复现示例
下面的代码清晰地展示了这个问题:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;
public class SimpleDateFormatThreadSafetyDemo {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
int threadCount = 10;
CountDownLatch latch = new CountDownLatch(threadCount);
String dateString = "2023-10-25 15:30:00";
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
// 所有线程都尝试解析同一个字符串
Date date = sdf.parse(dateString);
// 打印解析结果和当前线程,如果线程不安全,结果会五花八门
System.out.println(Thread.currentThread().getName() + " - Parsed date: " + date);
} catch (ParseException e) {
System.out.println(Thread.currentThread().getName() + " - Parse failed: " + e.getMessage());
} catch (NumberFormatException e) {
// 多线程下可能抛出的其他异常
System.out.println(Thread.currentThread().getName() + " - NumberFormatException: " + e.getMessage());
} finally {
latch.countDown();
}
}).start();
}
latch.await(); // 等待所有线程结束
System.out.println("All threads finished.");
}
}
运行结果可能如下(每次运行都可能不同):
Thread-2 - Parsed date: Wed Oct 25 15:30:00 CST 2023 // 正确
Thread-4 - Parsed date: Mon Nov 30 15:30:00 CST 2026 // 完全错误!
Thread-0 - Parse failed: For input string: ""
Thread-1 - NumberFormatException: multiple points
Thread-3 - Parsed date: Wed Oct 25 15:30:00 CST 2023 // 正确
...
你可以看到,在并发访问下,出现了:
- 解析出完全错误的日期。
- 抛出
ParseException。 - 抛出其他运行时异常,如
NumberFormatException。
解决方案
有几种常见的方法来解决这个多线程安全问题。
方案 1:局部变量(每次创建新实例)【推荐用于低并发】
最简单的方法是在每个需要使用的的方法内部创建新的 SimpleDateFormat 实例。
public Date parseDate(String dateString) throws ParseException {
// 每次调用都创建一个新的 SimpleDateFormat,线程私有,绝对安全
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.parse(dateString);
}
优点:简单直观,绝对线程安全。
缺点:如果方法被高频调用,会创建大量临时对象,增加 GC 压力,性能较差。
方案 2:使用 synchronized 加锁
将共享的 SimpleDateFormat 实例的访问用 synchronized 块保护起来。
public class DateUtils {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
public static Date parse(String dateString) throws ParseException {
synchronized (sdf) { // 使用类对象或sdf对象作为锁
return sdf.parse(dateString);
}
}
public static String format(Date date) {
synchronized (sdf) {
return sdf.format(date);
}
}
}
优点:避免了对象的频繁创建,复用了实例。
缺点:在高并发场景下,锁竞争会成为性能瓶颈。
方案 3:使用 ThreadLocal【最佳推荐】
这是兼顾性能和线程安全的最佳方案。它为每个线程提供一份独立的 SimpleDateFormat 实例副本,从而避免了竞争。
public class ThreadSafeDateFormatter {
private static final ThreadLocal<SimpleDateFormat> threadLocalDateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parse(String dateString) throws ParseException {
// get() 方法会返回当前线程独有的 SimpleDateFormat 实例
return threadLocalDateFormat.get().parse(dateString);
}
public static String format(Date date) {
return threadLocalDateFormat.get().format(date);
}
// 重要!如果使用线程池,在线程任务结束时最好清理 ThreadLocal,防止内存泄漏
public static void remove() {
threadLocalDateFormat.remove();
}
}
优点:
- 线程安全,每个线程有自己的副本,无竞争。
- 高性能,避免了频繁创建实例和锁竞争。
缺点: - 使用稍复杂。
- 需要注意内存泄漏问题(特别是在使用线程池时),在使用完毕后调用
remove()方法。
方案 4:切换到 Java 8 的 java.time 包【终极方案】
从 Java 8 开始,引入了全新的日期时间 API (java.time 包)。这些类(如 LocalDateTime, DateTimeFormatter) 是 不可变且线程安全的。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class Java8DateUtils {
// DateTimeFormatter 是线程安全的,可以放心定义为常量
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static LocalDateTime parse(String dateString) {
return LocalDateTime.parse(dateString, formatter); // 线程安全
}
public static String format(LocalDateTime dateTime) {
return formatter.format(dateTime); // 线程安全
}
}
这是现代 Java 开发的首选方案。
优点:
- 绝对线程安全。
- API 设计更清晰、更强大。
- 是 Java 官方的未来方向。
总结
| 方案 | 线程安全 | 性能 | 推荐度 |
|---|---|---|---|
| 局部变量 | 安全 | 差(对象创建开销) | ⭐⭐⭐ (简单场景) |
synchronized | 安全 | 中(有锁竞争) | ⭐⭐ ( legacy code ) |
ThreadLocal | 安全 | 高 | ⭐⭐⭐⭐ (维护旧项目时) |
Java 8 DateTimeFormatter | 安全 | 高 | ⭐⭐⭐⭐⭐ (新项目必选) |
最终建议:
- 如果是新项目,请毫不犹豫地使用 Java 8 的
java.timeAPI。 - 如果必须维护使用
SimpleDateFormat的旧代码,请使用ThreadLocal方案来修复多线程问题。
1077

被折叠的 条评论
为什么被折叠?



