SimpleDateFormat 多线程不安全是因其内部 Calendar 状态可变且操作非原子;DateTimeFormatter 不可变、线程安全,是 Java 8 起推荐替代方案。

SimpleDateFormat 在多线程下出错,不是偶然,而是设计使然——它内部依赖可变的 Calendar 对象,而 clear() 和 set() 操作非原子、无同步,多个线程一碰就乱。Java 8 起,DateTimeFormatter 是更自然、更安全的替代选择。
SimpleDateFormat 为何一并发就崩
它不是“状态无关”的工具类,而是持有并反复修改内部 calendar 字段:
-
parse()会调用calb.establish(calendar),该过程包含calendar.clear()和后续calendar.set(...)两步,中间状态可被其他线程读取或覆盖 -
format()同样先设时间再填充字段,若线程 A 刚设完年份、线程 B 就来读,可能拿到“2023-00-00”这类非法值 - 即使加
synchronized,也仅保护调用入口,无法约束内部 Calendar 的跨方法状态污染
ThreadLocal 封装:能用但有坑
用 ThreadLocal<simpledateformat></simpledateformat> 确实能隔离线程实例,但需格外注意:
- 必须在每次使用后(尤其在线程池场景)调用
threadLocal.remove(),否则造成内存泄漏 - 初始化时要统一配置时区、语言环境,例如:
withInitial(() -> { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); sdf.setTimeZone(TimeZone.getTimeZone("UTC")); return sdf; }) - 相比直接新建实例,它省了对象创建开销,但增加了管理复杂度,不推荐作为长期方案
DateTimeFormatter:线程安全的默认选项
Java 8 引入的 DateTimeFormatter 天然适合高并发:
- 不可变(immutable):所有属性 final,构造后不再改变,可放心定义为
static final - 无需 ThreadLocal 或锁:多个线程同时
format()或parse()互不干扰 - API 更清晰:配合
LocalDateTime、LocalDate使用,语义明确;如需转成旧式Date,用localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() - 模式严格但可控:默认拒绝多余空格或尾部字符,需宽松解析时可用
DateTimeFormatterBuilder显式配置
迁移时要注意的细节
从 SimpleDateFormat 切到 DateTimeFormatter,几个易错点要留心:
- 大小写敏感:“yyyy-MM-dd” 正确,“YYYY-MM-DD” 错——Y 表示 week-year,D 表示一年中的第几天
- 预定义常量可直接复用:
DateTimeFormatter.ISO_LOCAL_DATE_TIME、ISO_DATE等已优化且线程安全 - Spring Boot 中处理 JSON 日期:若前端传 “2023-01-01”,需配置 Jackson 的
@JsonFormat(pattern = "yyyy-MM-dd")或全局MappingJackson2HttpMessageConverter

















