news 2026/10/28 10:24:21

SimpleDateFormat 线程不安全实测:单线程 0 错,8 线程错 8 万次(附四组对照代码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SimpleDateFormat 线程不安全实测:单线程 0 错,8 线程错 8 万次(附四组对照代码)

2026-01-31。

这个日期不是我要的。

我要的是 1 月 1 日,或者 12 月 31 日。跑出来的是 1 月 31 日。月和日各取了一半,拼在一起。

代码里就一行:

privatestaticfinalSimpleDateFormatSDF=newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");

8 个线程,各格式化 2 万次。

第一遍跑完,错 98781 次。

我盯着这个数看了一会儿。98781,挺整齐。心里有点不踏实,又跑了一遍。

87187 次。

同一个 class 文件,同一个 JVM,同一台机器,中间隔了不到一分钟。错误次数自己都不稳定,这事儿就有点玄了。

说白了,这不是某个边界条件没处理,是个概率问题。线程抢得越乱,错得多还是错得少,全看当时调度。

先怀疑时区

看到日期串了,第一反应是时区。

以前被时区坑过,yyyy-MM-dd和YYYY-MM-dd差一个字母,跨年那一周直接错一年。所以我先回头翻代码,看有没有TimeZone.setDefault。

没有。

再看 JVM 参数,没有-Duser.timezone。容器里的时区是Asia/Shanghai,代码里没动过。

同一台机器,同一个 JVM,同一个时区,时区解释不了"1 月 31 日"这种日期。时区错了会差 8 小时,会跨天,但不会把 1 月 1 日和 12 月 31 日拼成 1 月 31 日。

排除。

再怀疑 Date 被改了

第二个想法:是不是Date对象在传的时候被谁改了。

Date是可变的,setTime()一调就变。如果多个线程共用一个Date,确实会出各种怪事。

但我这个测试里,两个Date是提前 parse 好的常量,每个线程从头到尾只读,一次setTime都没有。

而且真要是Date被改,错出来的应该是另一个完整日期,不会出现"1 月 31 日"这种两头拼的。

排除。

剩下就是那个 static

代码里能被多个线程同时碰的,只剩SDF这一个对象。

去看SimpleDateFormat的源码。它继承DateFormat,DateFormat里有个成员变量:

protectedCalendarcalendar;

format()干的事,是把传进来的Date写进这个calendar,再从calendar里把年月日时分秒一个一个读出来,拼成字符串。

写和读之间,隔着好几行代码。

两个线程同时进来,A 刚把calendar设成 1 月 1 日,还没读完,B 把它改成了 12 月 31 日。A 接着往下读,读到的月份是 12,日子还是 1。

拼出来就是 1 月 31 日。

这不是"数据被覆盖"那么简单,是一个对象的中间状态被另一个线程看见了。calendar这个字段,就是那个中间状态。

javadoc 里其实写了这句话:

Date formats are not synchronized. It is recommended to create separate format instances for each thread.

这段英文我很多年前就看过。当时扫一眼,觉得是"性能建议",没往心里去。

它不是性能建议。

四组对照,跑给你们看

光讲原理没意思,我索性写了四组,同一台机器连着跑:

A 单线程 + static SimpleDateFormat : 错误 0 B 8线程 + static SimpleDateFormat : 错误 87187 C 8线程 + ThreadLocal : 错误 0 D 8线程 + DateTimeFormatter : 错误 0 线程数=8 每线程=20000 单次总调用=160000 JDK=1.8.0_211

A 和 B 只差一个"单线程/多线程",一个 0 错,一个 8 万多错。

这就是为什么本地测不出来。你写单测,一个线程跑一万遍,全是绿的。压测一上,开始出错。等线上发现,错的数据已经进库了。

C 和 D 都是 0。格式化这件事本身没问题,问题出在把一个可变对象交给多个线程共用。

最容易漏的一点

它不抛异常。

如果是NullPointerException,上线当天就炸了,日志一堆,谁都知道出事了。这个不炸。

它返回的字符串长度对、格式对、能存库、能展示、能进对账单。2026-01-31 00:00:00,看起来完全正常。

你只能靠"这个日期不该出现在这里"去发现它。而大部分系统,没有人会去核对每一条日期。

我把Date换成String存的时候还想过,格式化这种东西能出什么错。现在知道了,它出的错比抛异常难查十倍。

三行改掉

新代码,直接换DateTimeFormatter:

privatestaticfinalDateTimeFormatterFMT=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

不可变,天生线程安全,不用 ThreadLocal,也不用每次 new。

老代码如果一时半会儿动不了,ThreadLocal兜一下:

privatestaticfinalThreadLocal<SimpleDateFormat>TL=ThreadLocal.withInitial(()->newSimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

注意remove()。线程池里线程是复用的,不 remove 就一直挂着,占着那个SimpleDateFormat不放。这个坑我踩过,反正内存就是涨,涨得莫名其妙,翻半天堆也看不出问题。

还有两个类

搜的时候别只搜SimpleDateFormat。

DecimalFormat一模一样,内部也是可变状态,也是线程不安全,也是不报错只算错。金额算错一分钱,对账的时候能查一天。

Calendar也是。它本身就是可变的,多线程共用照样出问题。

这三个类我在同一个项目里都见过,全是static final,全是"常量嘛,复用嘛"。

搜一下你们代码里有多少个static final SimpleDateFormat。搜出来先别急着改,数一下,再想想这些格式化每天跑多少次。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 10:22:36

酶活性:催化反应的“精准标尺”,读懂活性蛋白的转化密码

引言&#xff1a;蛋白质家族中的“催化剂”活性蛋白根据其功能特性&#xff0c;通常可分为三大类&#xff1a;结合类蛋白&#xff08;以特异性识别与结合为核心功能&#xff0c;如抗体、受体、配体&#xff09;、调控类蛋白&#xff08;负责调节细胞增殖、分化、迁移等行为&…

作者头像 李华
网站建设 2026/10/28 10:21:24

采矿智能机器人(7 种)

U-C-01 采煤工作面智能机器人群目录基本要求&#xff08;摘编&#xff09;&#xff1a;采煤机、刮板机、液压支架、转载、超前支架自主协同&#xff0c;煤岩界面识别。 主要AI 失效模式&#xff1a;支架落后造成空顶&#xff1b;采煤机割支架&#xff1b;人误入刮板&#xff1b…

作者头像 李华
网站建设 2026/10/28 10:21:22

动手学深度学习(李沐)——现代循环神经网络读书笔记

矩阵连续乘积可以导致梯度消失或梯度爆炸的问题。这导致我们的循环网络不能太长&#xff0c;但是在循环神经网络中早期观测值对预测所有未来观测值可能具有非常重要的意义&#xff0c;同时一些词元也可能没有相关的观测值。这里我们介绍以下现代循环神经网络解决这些问题。1.门…

作者头像 李华
网站建设 2026/10/28 10:21:11

2027年主流降AI工具对比:10款降AI率软件选型与复核

2027年主流降AI工具对比&#xff1a;10款降AI率软件选型与复核【工具功能与核对要点】BunnyScholar&#xff1a;比较长文处理与术语保留情况&#xff0c;逐段核对学术论证、引用和原意。助研君&#xff1a;比较分段处理、输出质量与费用&#xff0c;修改后保留版本并人工复核。…

作者头像 李华
网站建设 2026/10/28 10:21:07

2027年12款主流降AI率工具大汇总:论文降AIGC功能与复核指南

2027年12款主流降AI率工具大汇总&#xff1a;论文降AIGC功能与复核指南【自查与修订要点】先确认学校的检测范围、具体要求与提交期限。保存原稿、报告、研究笔记与版本记录。依据研究材料核对论证、术语、公式和引用。使用辅助工具前查看学校规定、费用与隐私条款。论文AIGC自…

作者头像 李华