问题

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 对象作为共享状态,多线程并发会导致数据错乱和异常。解决方案:

  1. ThreadLocal:每个线程独立实例(注意清理避免内存泄漏)
  2. DateTimeFormatter(Java 8+,最推荐):不可变、线程安全、性能更好
  3. 每次创建新实例(性能差)
  4. 加锁(并发性能差)

生产环境推荐使用 DateTimeFormatter 配合 LocalDateTime,完全避免线程安全问题。