SimpleDateFormat为什么线程不安全?源码级解析与解决方案
在 Java 面试中,除了 Spring 事务和 JVM 之外,SimpleDateFormat 也是高频考点之一。
很多人都知道:
SimpleDateFormat
是线程不安全的。
但真正的问题是:
- 为什么线程不安全?
- 哪里发生了线程安全问题?
- format() 方法不是只格式化时间吗?
- parse() 为什么会解析异常?
- 实际项目中如何解决?
本文将结合源码深入分析 SimpleDateFormat 线程安全问题,并介绍实际开发中的解决方案。
目录
- 什么是SimpleDateFormat
- 常见使用方式
- 线程安全问题复现
- 为什么线程不安全
- format源码分析
- parse源码分析
- Calendar导致的问题
- 多线程环境下的风险
- ThreadLocal解决方案
- DateTimeFormatter解决方案
- 面试高频问题
- 总结
一、什么是SimpleDateFormat
SimpleDateFormat 是 JDK 提供的日期格式化工具类。
常见使用:
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
格式化日期:
String result = sdf.format(new Date());
输出:
2026-06-07 10:30:15
解析日期:
Date date =
sdf.parse("2026-06-07 10:30:15");
因此:
SimpleDateFormat
=
日期 ↔ 字符串
转换工具。
二、单线程环境下没有问题
例如:
public static void main(String[] args) {
SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd");
String result =
sdf.format(new Date());
System.out.println(result);
}
运行正常。
很多开发者因此误以为:
SimpleDateFormat没有问题
实际上问题出现在:
多线程环境
三、线程安全问题复现
创建一个全局实例:
private static final SimpleDateFormat SDF =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
启动多个线程:
ExecutorService pool =
Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
pool.execute(() -> {
String result =
SDF.format(new Date());
System.out.println(result);
});
}
看起来没问题。
但是如果同时执行:
format()
和:
parse()
可能出现:
NumberFormatException
ParseException
日期错乱
等问题。
甚至可能出现:
2026-15-89
这种明显非法的日期。
四、为什么线程不安全
核心原因:
SimpleDateFormat内部维护了共享状态
最关键的成员变量:
private Calendar calendar;
查看源码:
public class SimpleDateFormat
extends DateFormat {
}
继续查看:
protected Calendar calendar;
说明:
所有format()
所有parse()
共用同一个Calendar对象
而:
Calendar
是可变对象。
这就是问题根源。
五、format源码分析
看JDK源码:
public final String format(Date date) {
return format(date,
new StringBuffer(),
DontCareFieldPosition.INSTANCE)
.toString();
}
继续进入:
private StringBuffer format(
Date date,
StringBuffer toAppendTo,
FieldDelegate delegate) {
}
里面有关键代码:
calendar.setTime(date);
也就是说:
每次调用:
sdf.format(date)
都会执行:
calendar.setTime(...)
修改内部Calendar状态。
六、多线程下的问题
假设:
线程A:
sdf.format(
"2026-01-01"
);
执行:
calendar.setTime(
2026–01–01
);
此时CPU切换。
线程B开始执行:
sdf.format(
"2027-12-31"
);
执行:
calendar.setTime(
2027–12–31
);
此时:
共享Calendar
已经变成
2027-12-31
然后CPU再次切回线程A。
线程A继续格式化:
calendar.get(...)
获取日期字段。
结果:
读到的是线程B修改后的数据
最终出现:
时间错乱
七、parse为什么更容易出问题
format一般只是读取数据。
而:
parse()
不仅会修改Calendar。
还会修改:
NumberFormat
等内部状态。
源码中:
calendar.clear();
calendar.set(...)
numberFormat.parse(...)
这些都是共享对象。
多个线程同时操作:
数据竞争
更加严重。
实际运行可能出现:
java.lang.NumberFormatException
例如:
multiple points
For input string
等异常。
八、一个典型错误案例
很多项目会这样写:
public class DateUtil {
private static final SimpleDateFormat SDF =
new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss");
}
然后:
DateUtil.SDF.format(...)
被全项目共享。
单线程测试:
正常
压测:
偶发错误
线上:
随机异常
最难排查。
九、解决方案一:每次创建新对象
最简单方案:
public String format(Date date) {
SimpleDateFormat sdf =
new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
优点:
线程安全
缺点:
频繁创建对象
性能一般。
十、解决方案二:ThreadLocal
实际项目常用方案。
定义:
private static final ThreadLocal<
SimpleDateFormat> THREAD_LOCAL =
ThreadLocal.withInitial(
() ->
new SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss"));
使用:
String result =
THREAD_LOCAL.get()
.format(new Date());
原理:
每个线程
拥有独立SimpleDateFormat
图示:
线程A
↓
SimpleDateFormatA
—————-
线程B
↓
SimpleDateFormatB
—————-
线程C
↓
SimpleDateFormatC
互不影响。
十一、解决方案三:DateTimeFormatter(推荐)
JDK8新增:
java.time
包。
推荐使用:
DateTimeFormatter
定义:
DateTimeFormatter formatter =
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss");
格式化:
String result =
LocalDateTime.now()
.format(formatter);
解析:
LocalDateTime.parse(
"2026-06-07 12:00:00",
formatter
);
为什么线程安全?
因为:
DateTimeFormatter
是:
不可变对象(Immutable)
创建后状态不会改变。
天然线程安全。
十二、三种方案对比
| 每次new | 是 | 一般 | ★★ |
| ThreadLocal | 是 | 较高 | ★★★★ |
| DateTimeFormatter | 是 | 高 | ★★★★★ |
实际项目推荐:
JDK8+
优先DateTimeFormatter
十三、面试高频问题
面试题1
SimpleDateFormat线程安全吗?
答案:
不安全
面试题2
为什么线程不安全?
答案:
内部共享Calendar对象
format()
parse()
都会修改Calendar状态
面试题3
format方法为什么会出现问题?
答案:
源码中:
calendar.setTime(...)
会修改共享状态。
面试题4
parse为什么更容易报错?
答案:
除了Calendar
还会修改NumberFormat
共享状态更多
面试题5
如何解决?
答案:
1. 每次new
2. ThreadLocal
3. DateTimeFormatter(推荐)
面试题6
JDK8为什么推荐DateTimeFormatter?
答案:
不可变对象
天然线程安全
十四、实际项目最佳实践
如果是:
JDK8及以上
直接使用:
DateTimeFormatter
例如:
private static final DateTimeFormatter FORMATTER =
DateTimeFormatter.ofPattern(
"yyyy-MM-dd HH:mm:ss");
如果是:
老项目
JDK7
推荐:
ThreadLocal<SimpleDateFormat>
不要这样写:
public static final SimpleDateFormat SDF =
new SimpleDateFormat(...);
因为:
高并发下必出问题
总结
本文深入分析了 SimpleDateFormat 线程安全问题。
核心原因:
内部共享Calendar对象
源码关键位置:
calendar.setTime(...)
导致多个线程同时修改状态。
因此:
SimpleDateFormat
不是线程安全的
解决方案:
牢记一句面试经典回答:
SimpleDateFormat线程不安全的根本原因是内部维护了可变的Calendar对象,format和parse过程中会修改共享状态,导致多线程环境下出现数据竞争问题。JDK8之后推荐使用DateTimeFormatter代替SimpleDateFormat。





