跳到主要内容

日期时间处理与线程安全

日期时间API概述

Java中的日期时间处理经历了多次演进,从最初的DateCalendar到Java 8引入的全新时间API。理解这些API的使用场景和线程安全特性,对于编写健壮的应用程序至关重要。

SimpleDateFormat基础用法

日期格式化与解析

SimpleDateFormat是传统的日期格式化工具,支持日期与字符串的相互转换。

/**
* SimpleDateFormat基础使用示例
*/
public class DateFormatBasicDemo {

public static void main(String[] args) throws ParseException {
// 创建格式化器
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

// Date转String(格式化)
Date now = new Date();
String dateStr = sdf.format(now);
System.out.println("格式化结果: " + dateStr);
// 输出: 格式化结果: 2024-12-02 15:30:45

// String转Date(解析)
String input = "2024-06-15 10:20:30";
Date parsedDate = sdf.parse(input);
System.out.println("解析结果: " + parsedDate);
}
}

常用日期格式模式

字符含义示例
yyyy四位年份2024
MM两位月份01-12
dd两位日期01-31
HH24小时制小时00-23
hh12小时制小时01-12
mm分钟00-59
ss00-59
SSS毫秒000-999
E星期周一
a上午/下午AM/PM

常用格式示例:

public class DateFormatPatterns {

public static void main(String[] args) {
Date now = new Date();

// 各种格式示例
printFormat("yyyy-MM-dd", now); // 2024-12-02
printFormat("yyyy/MM/dd HH:mm:ss", now); // 2024/12/02 15:30:45
printFormat("yyyy年MM月dd日", now); // 2024年12月02日
printFormat("yyyy-MM-dd HH:mm:ss.SSS", now); // 2024-12-02 15:30:45.123
printFormat("yyyy-MM-dd E", now); // 2024-12-02 周一
printFormat("yyyy-MM-dd hh:mm:ss a", now); // 2024-12-02 03:30:45 下午
}

private static void printFormat(String pattern, Date date) {
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
System.out.println(pattern + " => " + sdf.format(date));
}
}

时区处理

/**
* 不同时区的时间转换
*/
public class TimeZoneDemo {

public static void main(String[] args) {
Date now = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");

// 北京时间(东八区)
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
System.out.println("北京时间: " + sdf.format(now));

// 东京时间(东九区)
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
System.out.println("东京时间: " + sdf.format(now));

// 纽约时间(西五区)
sdf.setTimeZone(TimeZone.getTimeZone("America/New_York"));
System.out.println("纽约时间: " + sdf.format(now));

// 伦敦时间(零时区)
sdf.setTimeZone(TimeZone.getTimeZone("Europe/London"));
System.out.println("伦敦时间: " + sdf.format(now));
}
}

SimpleDateFormat线程安全问题

问题现象

SimpleDateFormat是非线程安全的,在多线程环境下共享使用会导致数据错误。

/**
* 演示SimpleDateFormat的线程安全问题
*/
public class DateFormatThreadSafetyIssue {

// 危险:作为共享变量使用
private static SimpleDateFormat sharedFormat =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static void main(String[] args) throws InterruptedException {
// 使用线程安全的Set收集结果
Set<String> results = Collections.synchronizedSet(new HashSet<>());
CountDownLatch latch = new CountDownLatch(100);

ExecutorService executor = Executors.newFixedThreadPool(10);

for (int i = 0; i < 100; i++) {
int dayOffset = i;
executor.submit(() -> {
try {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, dayOffset);

// 多线程同时使用共享的SimpleDateFormat
String dateStr = sharedFormat.format(cal.getTime());
results.add(dateStr);
} finally {
latch.countDown();
}
});
}

latch.await();
executor.shutdown();

// 预期应该有100个不同的日期,但实际结果小于100
System.out.println("预期结果数: 100");
System.out.println("实际结果数: " + results.size());
// 输出可能是:实际结果数: 87(数据丢失或错乱)
}
}

问题根因分析

查看SimpleDateFormat.format()源码:

// SimpleDateFormat中的format方法
private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) {
// 使用成员变量calendar保存时间
calendar.setTime(date); // 问题所在!

// 后续基于calendar进行格式化
// ...
}

当多个线程同时调用format()时,可能发生:

  1. 线程A调用calendar.setTime()设置时间为2024-01-01
  2. 线程B调用calendar.setTime()设置时间为2024-12-31
  3. 线程A继续执行,但读取到的是线程B设置的时间

解决方案

方案一:使用局部变量

每次使用时创建新的实例,避免共享。

public class LocalVariableSolution {

public String formatDate(Date date) {
// 每次创建新实例
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}

public Date parseDate(String dateStr) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(dateStr);
}
}

优点: 简单直接,线程安全
缺点: 频繁创建对象,有一定性能开销

方案二:加同步锁

对共享的SimpleDateFormat加锁。

public class SynchronizedSolution {

private static final SimpleDateFormat FORMAT =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

public static String formatDate(Date date) {
synchronized (FORMAT) {
return FORMAT.format(date);
}
}

public static Date parseDate(String dateStr) throws ParseException {
synchronized (FORMAT) {
return FORMAT.parse(dateStr);
}
}
}

优点: 避免重复创建对象
缺点: 锁竞争可能成为性能瓶颈

方案三:使用ThreadLocal

为每个线程维护独立的实例。

/**
* ThreadLocal解决方案
* 每个线程拥有独立的SimpleDateFormat实例
*/
public class ThreadLocalSolution {

private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

public static String formatDate(Date date) {
return DATE_FORMAT.get().format(date);
}

public static Date parseDate(String dateStr) throws ParseException {
return DATE_FORMAT.get().parse(dateStr);
}

// 使用完毕后清理(特别是线程池场景)
public static void cleanup() {
DATE_FORMAT.remove();
}
}

// 使用示例
public class ThreadLocalDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);

for (int i = 0; i < 100; i++) {
executor.submit(() -> {
try {
String result = ThreadLocalSolution.formatDate(new Date());
System.out.println(Thread.currentThread().getName() + ": " + result);
} finally {
// 线程池场景需要清理
ThreadLocalSolution.cleanup();
}
});
}

executor.shutdown();
}
}

优点: 兼顾线程安全和性能
缺点: 需要注意内存泄漏问题

方案四:使用DateTimeFormatter(推荐)

Java 8引入的DateTimeFormatter是线程安全的,是处理日期格式化的最佳选择。

/**
* DateTimeFormatter解决方案(推荐)
* 天然线程安全,不可变对象
*/
public class DateTimeFormatterSolution {

// 可以安全地作为共享常量
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

/**
* 格式化LocalDateTime
*/
public static String format(LocalDateTime dateTime) {
return dateTime.format(FORMATTER);
}

/**
* 解析字符串为LocalDateTime
*/
public static LocalDateTime parse(String dateStr) {
return LocalDateTime.parse(dateStr, FORMATTER);
}

/**
* Date转String
*/
public static String formatDate(Date date) {
LocalDateTime ldt = date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime();
return ldt.format(FORMATTER);
}

/**
* String转Date
*/
public static Date parseToDate(String dateStr) {
LocalDateTime ldt = LocalDateTime.parse(dateStr, FORMATTER);
return Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
}
}

Java 8时间API

核心类介绍

常用操作示例

/**
* Java 8时间API使用示例
*/
public class Java8DateTimeDemo {

public static void main(String[] args) {
// 获取当前日期时间
LocalDate today = LocalDate.now();
LocalTime now = LocalTime.now();
LocalDateTime dateTime = LocalDateTime.now();

System.out.println("今天: " + today);
System.out.println("现在: " + now);
System.out.println("日期时间: " + dateTime);

// 创建指定日期
LocalDate birthday = LocalDate.of(1995, 6, 15);
LocalDateTime meeting = LocalDateTime.of(2024, 12, 25, 14, 30);

// 日期计算
LocalDate nextWeek = today.plusWeeks(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDateTime twoHoursLater = dateTime.plusHours(2);

System.out.println("下周: " + nextWeek);
System.out.println("上个月: " + lastMonth);
System.out.println("两小时后: " + twoHoursLater);

// 日期比较
boolean isAfter = today.isAfter(birthday);
boolean isBefore = today.isBefore(LocalDate.of(2025, 1, 1));

// 计算间隔
Period period = Period.between(birthday, today);
System.out.printf("年龄: %d年%d月%d天%n",
period.getYears(), period.getMonths(), period.getDays());

// 格式化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm");
String formatted = dateTime.format(formatter);
System.out.println("格式化: " + formatted);

// 解析
LocalDateTime parsed = LocalDateTime.parse("2024年12月25日 10:30", formatter);
System.out.println("解析: " + parsed);
}
}

时区处理

/**
* 时区转换示例
*/
public class ZonedDateTimeDemo {

public static void main(String[] args) {
// 当前时区的时间
ZonedDateTime beijingTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
System.out.println("北京时间: " + beijingTime);

// 转换到其他时区
ZonedDateTime tokyoTime = beijingTime.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
ZonedDateTime newYorkTime = beijingTime.withZoneSameInstant(ZoneId.of("America/New_York"));

System.out.println("东京时间: " + tokyoTime);
System.out.println("纽约时间: " + newYorkTime);

// 格式化带时区的输出
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
System.out.println("格式化北京时间: " + beijingTime.format(formatter));
System.out.println("格式化纽约时间: " + newYorkTime.format(formatter));
}
}

方案对比总结

方案线程安全性能推荐度
局部变量安全较低(频繁创建)一般
同步锁安全较低(锁竞争)不推荐
ThreadLocal安全较高推荐
DateTimeFormatter安全最高强烈推荐

最佳实践建议:

  • 新项目优先使用Java 8的DateTimeFormatter
  • 旧项目改造可以使用ThreadLocal方案
  • 避免将SimpleDateFormat作为共享变量使用
  • 注意ThreadLocal在线程池场景下的内存泄漏问题

掌握日期时间处理的线程安全问题,能够有效避免生产环境中难以排查的并发Bug。