simpledateformat.parse() 线程不安全,因内部 calendar 状态被并发修改导致 numberformatexception 或乱序日期;应避免复用实例,推荐 java 8+ 的线程安全 datetimeformatter。

SimpleDateFormat.parse() 报错 java.lang.NumberFormatException 或乱序日期
这是最典型的线程不安全表现:多个线程共用同一个 SimpleDateFormat 实例调用 parse() 时,内部的 calendar 字段被并发修改,导致解析出错或返回错误日期(比如把 "2023-01-01" 解成 2023-12-01)。不是每次必现,但压测一跑就崩。
根本原因是 SimpleDateFormat 不是无状态的——它把解析中间状态存在可变的 Calendar 里,而这个对象没做同步保护。
- 别在类字段里声明
SimpleDateFormat并复用,哪怕加了synchronized也容易漏锁或拖慢吞吐 - 不要用
ThreadLocal<simpledateformat></simpledateformat>手动管理,容易内存泄漏(线程池线程不销毁,ThreadLocal不清理) - 如果必须用旧 API,至少确保每个解析/格式化操作都新建实例:
new SimpleDateFormat("yyyy-MM-dd")—— 虽然稍慢,但安全、简单、可预测
Java 8+ 推荐用 DateTimeFormatter 替代
DateTimeFormatter 是不可变(immutable)且线程安全的,所有预定义常量(如 DateTimeFormatter.ISO_LOCAL_DATE)和自定义实例(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))都能放心共享。
注意迁移时的几个关键差异:
-
parse()返回的是LocalDateTime/LocalDate等类型,不是Date;需要转Date时用localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() - 模式字母大小写敏感:
"yyyy-MM-dd"正确,"YYYY-MM-DD"会解析出错(Y是 week-year,D是年中第几天) - 默认不接受空格或多余字符,比如
"2023-01-01 "会抛DateTimeParseException;需显式用DateTimeFormatterBuilder配置宽松解析
Spring Boot 项目里怎么统一处理日期参数
如果用 @RequestBody 接收 JSON,Jackson 默认对 LocalDateTime 用 ISO_LOCAL_DATE_TIME 格式;但前端传的是 "2023-01-01" 这种字符串,就会反序列化失败。
正确做法是配置 Jackson 的模块:
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
JavaTimeModule module = new JavaTimeModule();
module.addDeserializer(LocalDateTime.class,
new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
module.addDeserializer(LocalDate.class,
new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
mapper.registerModule(module);
return mapper;
}
- 别在实体字段上加
@JsonFormat(pattern = "..."),分散难维护,且不生效于嵌套对象 - 避免用
String接收再手动 parse,绕过类型安全,还留着SimpleDateFormat的隐患 - 如果接口要兼容多种格式(如 "2023/01/01" 和 "2023-01-01"),得写自定义
JsonDeserializer,而不是靠DateTimeFormatter多模式 fallback
Logback 或日志框架里 SimpleDateFormat 怎么办
Logback 的 %d{yyyy-MM-dd HH:mm:ss} 内部其实用了线程安全的 DateTimeFormatter(新版),但老版本或自定义 Converter 里若手写了 SimpleDateFormat,就会成为隐藏的并发雷区。
检查点:
- 确认 Logback 版本 ≥ 1.2.0(对应 SLF4J 1.7.25+),旧版
ClassicConverter可能仍用SimpleDateFormat - 自定义日志格式器时,直接用
DateTimeFormatter.ofPattern(...)创建静态 final 实例,别碰SimpleDateFormat - 如果用
AsyncAppender,更要小心——日志事件可能跨线程传递,任何共享可变格式器都会出问题
线程安全不是“大概率不出事”,而是状态是否可预测。只要一个地方复用了 SimpleDateFormat,整个服务的稳定性就取决于运气。替换成本不高,但漏掉一个地方,就可能在线上安静地错上几个月。










