在写 Java 程序时,处理日期ref="/tag/287/" style="color:#EB6E00;font-weight:bold;">格式化几乎是家常便饭。比如生成日志时间戳、导出报表的创建时间、订单详情页的时间展示,都离不开 SimpleDateFormat。但你可能没意识到,这个看似无害的工具,在多线程环境下会悄悄埋下雷。
问题出在哪?
SimpleDateFormat 不是线程安全的。多个线程共用同一个实例进行格式化操作时,轻则返回错误的时间,重则直接抛出异常。这就像几个人同时往一个记事本上写字,最后谁也看不懂上面写了啥。
举个常见场景:你在做一个高并发的订单系统,每次下单都要记录创建时间并格式化成 yyyy-MM-dd HH:mm:ss。如果所有请求都用同一个 SimpleDateFormat 实例,不出几天就会收到报警——时间显示成了 2099 年,或者直接报 NumberFormatException。
怎么解决?
最简单的办法是每次用的时候新建一个实例:
public String formatDate(Date date) {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
}
这招确实能避坑,但代价是频繁创建对象,GC 压力大。在高并发场景下,性能损耗明显。
更常见的做法是用 ThreadLocal,每个线程独享自己的格式化器:
private static final ThreadLocal<SimpleDateFormat> DATE_FORMATTER =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public String formatDate(Date date) {
return DATE_FORMATTER.get().format(date);
}
这样既避免了线程冲突,又减少了对象创建。不过要注意,如果用在线程池环境,记得在使用完后调用 remove(),防止内存泄漏。
更好的选择:用新的时间 API
从 Java 8 开始,推荐使用 java.time.format.DateTimeFormatter。它是不可变对象,天生线程安全,用起来也更清爽。
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public String formatDate(LocalDateTime time) {
return FORMATTER.format(time);
}
不需要额外同步,也不用担心并发问题。只要把原来的 Date 换成 LocalDateTime,升级几乎无痛。
如果你还在维护老项目,一时没法切换到新 API,至少确保别在多线程里共用 SimpleDateFormat。一个小疏忽,可能就是生产环境半夜被叫醒的原因。