问题
SimpleDateFormat是线程安全的吗?使用时应该注意什么?
答案
核心结论
SimpleDateFormat不是线程安全的,多线程并发使用会导致数据错乱、异常抛出等问题。
线程不安全演示
public class DateFormatTest {
// ❌ 错误:多线程共享SimpleDateFormat
private static final SimpleDateFormat SDF =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
// 10个线程并发格式化日期
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
Date date = SDF.parse("2024-01-01 12:00:00");
System.out.println(date);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
}
}
// 可能的输出(数据错乱)
// Mon Jan 01 12:00:00 CST 2024 ✅ 正确
// Sat Feb 01 12:00:00 CST 2024 ❌ 错误(月份变了)
// java.lang.NumberFormatException ❌ 异常
// null ❌ 空值
根本原因
1. 内部可变状态
SimpleDateFormat内部维护了可变的 Calendar 对象:
// SimpleDateFormat源码(简化)
public class SimpleDateFormat extends DateFormat {
private Calendar calendar; // ← 可变共享状态
public Date parse(String text) throws ParseException {
calendar.clear(); // ← 线程1执行到这里
// ... 解析逻辑
calendar.set(...); // ← 线程2修改了calendar
return calendar.getTime(); // ← 线程1拿到错误数据
}
public StringBuffer format(Date date, ...) {
calendar.setTime(date); // ← 修改共享状态
// ... 格式化逻辑
}
}
2. 竞态条件
// 时间线演示
时间点1: 线程A调用parse("2024-01-01") → calendar.clear()
时间点2: 线程B调用parse("2024-12-31") → calendar.clear() ← 覆盖线程A的状态
时间点3: 线程A继续执行 → calendar.set(2024, 0, 1)
时间点4: 线程B继续执行 → calendar.set(2024, 11, 31) ← 又覆盖了
时间点5: 线程A返回结果 → 拿到线程B的数据!❌
解决方案
方案1:每次创建新实例(不推荐)
// ❌ 性能差:每次都创建对象
public String formatDate(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(date);
}
缺点:频繁创建对象,性能开销大。
方案2:ThreadLocal(常用方案)
// ✅ 推荐:使用ThreadLocal
public class DateUtils {
private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String format(Date date) {
return THREAD_LOCAL.get().format(date);
}
public static Date parse(String dateStr) throws ParseException {
return THREAD_LOCAL.get().parse(dateStr);
}
}
// 使用
String formatted = DateUtils.format(new Date());
Date parsed = DateUtils.parse("2024-01-01 12:00:00");
原理:每个线程拥有独立的SimpleDateFormat实例。
注意事项:
// ⚠️ 在线程池场景下需要清理ThreadLocal
public class DateUtils {
private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static String format(Date date) {
try {
return THREAD_LOCAL.get().format(date);
} finally {
// 清理ThreadLocal,避免内存泄漏
THREAD_LOCAL.remove();
}
}
}
方案3:加锁(性能差)
// ⚠️ 性能瓶颈:锁竞争激烈
public class DateUtils {
private static final SimpleDateFormat SDF =
new SimpleDateFormat("yyyy-MM-dd");
public static synchronized String format(Date date) {
return SDF.format(date);
}
public static synchronized Date parse(String dateStr)
throws ParseException {
return SDF.parse(dateStr);
}
}
缺点:并发性能差,不推荐。
方案4:DateTimeFormatter(Java 8+,最推荐)
// ✅ 最佳方案:使用DateTimeFormatter(线程安全)
public class DateUtils {
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static String format(LocalDateTime dateTime) {
return dateTime.format(FORMATTER);
}
public static LocalDateTime parse(String dateStr) {
return LocalDateTime.parse(dateStr, FORMATTER);
}
}
// 使用
String formatted = DateUtils.format(LocalDateTime.now());
LocalDateTime parsed = DateUtils.parse("2024-01-01 12:00:00");
优势:
- 线程安全:不可变对象
- 性能好:无锁设计
- API更友好:链式调用
SimpleDateFormat vs DateTimeFormatter
| 维度 | SimpleDateFormat | DateTimeFormatter |
|---|---|---|
| 线程安全 | ❌ 否 | ✅ 是 |
| 可变性 | 可变对象 | 不可变对象 |
| 性能 | 需ThreadLocal | 可直接共享 |
| API | 较复杂 | 链式调用、更清晰 |
| 时区处理 | 较弱 | 更强大 |
| Java版本 | Java 1.1+ | Java 8+ |
DateTimeFormatter完整示例
public class DateTimeUtils {
// 日期格式化器
private static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 日期时间格式化器
private static final DateTimeFormatter DATETIME_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 带时区的格式化器
private static final DateTimeFormatter ZONED_FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z");
// LocalDate → String
public static String formatDate(LocalDate date) {
return date.format(DATE_FORMATTER);
}
// String → LocalDate
public static LocalDate parseDate(String dateStr) {
return LocalDate.parse(dateStr, DATE_FORMATTER);
}
// LocalDateTime → String
public static String formatDateTime(LocalDateTime dateTime) {
return dateTime.format(DATETIME_FORMATTER);
}
// String → LocalDateTime
public static LocalDateTime parseDateTime(String dateTimeStr) {
return LocalDateTime.parse(dateTimeStr, DATETIME_FORMATTER);
}
// Date → LocalDateTime(兼容老代码)
public static LocalDateTime toLocalDateTime(Date date) {
return date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
}
// LocalDateTime → Date(兼容老代码)
public static Date toDate(LocalDateTime dateTime) {
return Date.from(dateTime
.atZone(ZoneId.systemDefault())
.toInstant());
}
}
常见陷阱
陷阱1:静态字段共享
// ❌ 错误:静态字段被多线程共享
public class UserService {
private static final SimpleDateFormat SDF =
new SimpleDateFormat("yyyy-MM-dd");
public void processUser(User user) {
String birthDate = SDF.format(user.getBirthDate()); // 线程不安全
}
}
陷阱2:Spring Bean单例
// ❌ 错误:Spring Bean默认单例,多线程共享
@Component
public class DateFormatter {
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
public String format(Date date) {
return sdf.format(date); // 线程不安全
}
}
陷阱3:忽略异常
// ❌ 危险:并发情况下可能抛出异常
try {
Date date = sharedSDF.parse(dateStr);
} catch (ParseException e) {
// 可能捕获到NumberFormatException、ArrayIndexOutOfBoundsException等
}
实战最佳实践
Spring Boot项目配置
@Configuration
public class DateConfig {
// ✅ 使用Jackson配置全局日期格式
@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
return builder -> {
builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
builder.serializerByType(LocalDateTime.class,
new LocalDateTimeSerializer(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
builder.deserializerByType(LocalDateTime.class,
new LocalDateTimeDeserializer(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
};
}
}
工具类封装
public class DateUtils {
// ✅ 使用DateTimeFormatter(线程安全)
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 格式化当前时间
public static String now() {
return LocalDateTime.now().format(FORMATTER);
}
// 格式化指定时间
public static String format(LocalDateTime dateTime) {
return dateTime == null ? "" : dateTime.format(FORMATTER);
}
// 解析字符串
public static LocalDateTime parse(String dateTimeStr) {
if (dateTimeStr == null || dateTimeStr.isEmpty()) {
return null;
}
return LocalDateTime.parse(dateTimeStr, FORMATTER);
}
// 兼容老代码:Date转String
public static String formatDate(Date date) {
if (date == null) return "";
return format(toLocalDateTime(date));
}
private static LocalDateTime toLocalDateTime(Date date) {
return date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
}
}
性能对比测试
// JMH基准测试(百万次操作)
@State(Scope.Thread)
public class DateFormatBenchmark {
private Date date = new Date();
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
private DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
private LocalDateTime ldt = LocalDateTime.now();
@Benchmark
public String testSimpleDateFormat() {
return sdf.format(date);
}
@Benchmark
public String testDateTimeFormatter() {
return ldt.format(dtf);
}
}
// 典型结果
// testSimpleDateFormat: 1.2 μs/op
// testDateTimeFormatter: 0.8 μs/op (快50%)
答题总结
SimpleDateFormat 不是线程安全的,原因是内部使用可变的 Calendar 对象作为共享状态,多线程并发会导致数据错乱和异常。解决方案:
- ThreadLocal:每个线程独立实例(注意清理避免内存泄漏)
- DateTimeFormatter(Java 8+,最推荐):不可变、线程安全、性能更好
- 每次创建新实例(性能差)
- 加锁(并发性能差)
生产环境推荐使用 DateTimeFormatter 配合 LocalDateTime,完全避免线程安全问题。