在多线程环境下,日期时间处理是线程安全问题的高发区。Java8 之前的日期时间类(如 SimpleDateFormat、Calendar)存在严重的线程安全问题,主要原因是它们的内部状态可变,在多线程并发访问时会导致数据错乱。而 Java8 引入的新日期/时间 API 不允许修改内部状态,如 LocalData、LocalDataTime 等,每次修改均会返回一个新的实例,从根本上解决了这些问题。
下面以 SimpleDateFormat 为例,演示 SimpleDateFormat 存在的线程安全隐患。代码如下:
package com.hxstrive.java_date_time.thread; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class SDFThreadSafeDemo { // 共享的SimpleDateFormat实例 private static final SimpleDateFormat SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(10); // 10个线程同时使用SDF格式化日期 for (int i = 0; i < 10; i++) { int finalI = i; executor.submit(() -> { try { Date date = new Date(1000L * 60 * 60 * 24 * finalI); System.out.println(SDF.format(date)); } catch (Exception e) { e.printStackTrace(); } }); } executor.shutdown(); } }
运行结果:
1970-01-04 08:00:00 1970-01-01 08:00:00 1970-01-04 08:00:00 1970-01-04 08:00:00 1970-01-01 08:00:00 1970-01-05 08:00:00 1970-01-05 08:00:00 1970-01-04 08:00:00 1970-01-04 08:00:00 1970-01-04 08:00:00
从运行结果可知,出现重复日期。还有可能抛出 NumberFormatException 或 IllegalArgumentException。导致这些问题的根本原因是 SimpleDateFormat 内部使用 Calendar 对象维护状态,多线程并发修改导致状态混乱。如下图:
为解决线程安全问题,有如下方案:
每次使用时创建新实例:频繁创建销毁对象,性能开销大。
使用 synchronized 同步:多线程竞争锁,导致性能下降。
ThreadLocal 存储:需要手动管理生命周期,代码复杂且易内存泄漏。
Java8 引入的 java.time 包(如 LocalDateTime、DateTimeFormatter)采用不可变对象设计,从根本上保证了线程安全:
不可变对象特性:对象创建后状态无法修改,所有修改操作都返回新对象。因此,无需担心并发修改导致的状态不一致。
核心线程安全类:
LocalDate、LocalTime、LocalDateTime:日期时间对象
ZonedDateTime:带时区的日期时间对象
DateTimeFormatter:日期时间格式化器
Instant:时间戳对象
简单示例:
package com.hxstrive.java_date_time.thread; import java.time.Instant; import java.time.format.DateTimeFormatter; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ModernDateTimeThreadSafe { // 共享的DateTimeFormatter实例(线程安全) private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(10); // 10个线程同时使用共享的格式化器 for (int i = 0; i < 10; i++) { int finalI = i; executor.submit(() -> { try { // 创建时间戳(不可变对象) Instant instant = Instant.EPOCH.plusSeconds(60 * 60 * 24L * finalI); // 格式化操作(线程安全) String result = FORMATTER.format(instant.atZone(java.time.ZoneId.systemDefault())); System.out.println(result); } catch (Exception e) { e.printStackTrace(); } }); } executor.shutdown(); } }
运行结果:
1970-01-05 08:00:00 1970-01-08 08:00:00 1970-01-09 08:00:00 1970-01-03 08:00:00 1970-01-06 08:00:00 1970-01-04 08:00:00 1970-01-01 08:00:00 1970-01-07 08:00:00 1970-01-02 08:00:00 1970-01-10 08:00:00
从结果可知,输出的日期没有重复,是预期的结果。这是因为 DateTimeFormatter 是不可变的,可安全地作为静态常量在多线程间共享。