SimpleDateFormat线程不安全,多线程共享会导致格式错乱、异常或错误解析;应避免单例,改用ThreadLocal或Java 8+的DateTimeFormatter;注意大小写敏感、lenient模式、时区设置等细节。

SimpleDateFormat线程不安全,多线程下必须避免共享实例
SimpleDateFormat不是线程安全的,多个线程共用同一个SimpleDateFormat对象时,可能出现格式化结果错乱、抛出java.lang.ArrayIndexOutOfBoundsException或解析出错误日期(比如2023年变成1923年)。这不是偶发bug,而是内部使用的Calendar和字符缓冲区被并发修改导致的。
常见错误写法:
public class DateUtils {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String format(Date date) { return sdf.format(date); }
}
这种单例模式在Web应用中极易出问题。
- 正确做法:每次使用都新建实例(适合低频调用)
- 高频场景推荐用
ThreadLocal隔离实例 - Java 8+ 更建议直接迁移到
DateTimeFormatter(不可变、线程安全)
pattern字母大小写敏感,常见误写会导致意外输出
SimpleDateFormat的模式字符串对大小写极其敏感,例如mm是分钟,MM是月份;hh是12小时制小时,HH是24小时制;yy是两位年份,yyyy是四位年份。写错一个字母,结果可能完全不对,且不报错。
典型误用:
new SimpleDateFormat("yyyy-mm-dd") // 错!mm是分钟,月份应为MM
结果:2023-05-12 → 格式化成"2023-12-12"(把月份当成了分钟)
-
MMM输出英文缩写(Jan),MMMM输出全称(January) - 中文环境需显式设置
Locale.CHINA,否则EEE可能输出英文周几 - 想输出毫秒用
SSS,不是sss或SS
parse()方法默认宽松解析,可能掩盖输入错误
SimpleDateFormat默认启用lenient = true,这意味着它会“尽力”解析看似非法的日期,比如"2023-13-01"会被转成2024-01-01,"2023-02-30"变成2023-03-02。这在数据校验场景下非常危险。
解决方式很简单:
立即学习“Java免费学习笔记(深入)”;
- 调用
sdf.setLenient(false)关闭宽松模式 - 此时
parse()遇到非法日期会立即抛出ParseException - 注意:关闭后
"2023/1/1"这类非严格匹配的字符串也会失败,需确保输入格式与pattern完全一致
时区问题常被忽略,服务器本地时区可能干扰结果
SimpleDateFormat默认使用JVM启动时的默认时区(通常是服务器所在时区),而非UTC或用户期望时区。比如服务器在东八区,但你想解析ISO 8601时间"2023-01-01T00:00:00Z",不设时区会当成东八区时间处理,导致时间偏移8小时。
关键操作:
- 用
sdf.setTimeZone(TimeZone.getTimeZone("UTC"))显式指定 - 若要兼容带时区的输入(如
"2023-01-01T12:00:00+0800"),pattern里必须包含Z或XXX,且setTimeZone仍需配合使用 - Java 8+ 的
Instant.parse()或ZonedDateTime.parse()更直观,自动处理时区语义
真正麻烦的从来不是写对一行格式化代码,而是上线后发现定时任务在跨时区服务器上跑偏了两小时,或者日志时间戳批量错位——这些细节在开发机上根本暴露不出来。










