simpledateformat 不是线程安全的,因其内部 calendar 可变、解析位置与缓存字段无保护、操作非原子,导致 numberformatexception、arrayindexoutofboundsexception 或静默错乱;修复应优先用 datetimeformatter,次选 threadlocal 或局部变量。

SimpleDateFormat 不是线程安全的,这是 JDK 官方明确标注的事实——从 Java 8 开始,其 Javadoc 直接声明为 “not thread-safe”。只要多个线程共享同一个实例(比如定义为 static final),就可能在 parse() 或 format() 过程中触发状态污染,造成解析错乱、数字异常甚至崩溃。
核心问题:内部状态被并发篡改
它的问题不是“偶尔出错”,而是设计上就无法承受并发访问:
-
Calendar 对象可变且共享:每个 SimpleDateFormat 持有一个 Calendar 实例,在 parse 时反复调用
clear()、setTime()、add(),所有字段(如fields[]、isset[])都可被任意线程覆盖 -
解析位置和缓存未加保护:像
begin、end(当前解析索引)、parsedNumber(临时数字缓存)等字段,既无锁也无原子性保障 -
操作非原子:例如
cal.clear(); cal.set(...)是两步,中间可能被其他线程插入执行,导致日历状态半截错乱
典型异常表现
这些底层冲突会直接反映为运行时错误:
-
NumberFormatException:线程 A 写入parsedNumber=2023,线程 B 覆盖为12,A 继续用12当年份计算,抛出异常 -
ArrayIndexOutOfBoundsException:字段数组越界,常因pos索引被并发修改而跳到非法位置 - 静默错乱:不报错但结果错误,比如 “2023-01-01” 解析成 “2022-12-31”,这类问题最难排查,往往只在高并发生产环境暴露
常见误用场景
这些写法看似省事,实则埋雷:
- Spring Bean 中定义
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd") - Servlet 或 Netty Handler 里复用单个实例处理多个请求
- Flink/Spark UDF 中作为静态成员解析时间字段
- 单元测试里用固定线程池反复调用,却没加同步或隔离
真正可用的修复方式
别再靠“好像没出过问题”赌运气,稳妥方案就这几个:
-
优先升级到 DateTimeFormatter:Java 8+ 的
DateTimeFormatter是不可变、无状态、天然线程安全的,一行替换即可,推荐作为默认选择 - ThreadLocal 包装:每个线程独享一个实例,避免创建开销又规避竞争,适合不能立刻升级的老项目
- 局部变量新建:方法内每次 new,简单直接;JVM 对短生命周期对象优化很好,实际内存压力可控
- 必要时加 synchronized:仅限低频调用场景,高并发下会成为性能瓶颈,慎用










