StackTraces
Java已验证

SimpleDateFormat 多线程报 NumberFormatException

@debug_master32 天前更新阅读 6 min00

这个坑最阴险的地方在于:报错是间歇性的,日志要么抛一个 NumberFormatException: multiple points,要么不报错、静默返回一个错得离谱的日期(比如把 2016 解析成 2200 或 0002 年)。既不像异常那样好发现,又不像 bug 那样好复现,只在并发压力下才露头。

Issue & Env(案发现场与环境)

  • 现象:SimpleDateFormat 作为 static final 共享实例,多线程下偶发解析失败或日期串台,错误断断续续。
  • 环境:Java 8 之前(尤其常用老 SimpleDateFormat 的场景),或仍沿用 SimpleDateFormat 的存量代码。

StackTrace(堆栈日志)

标准报错是数字解析失败:

java.lang.NumberFormatException: multiple points
	at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1110)
	at java.lang.Double.parseDouble(Double.java:540)
	at java.text.DigitList.getDouble(DigitList.java:168)
	at java.text.DecimalFormat.parse(DecimalFormat.java:1321)
	at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2088)
	at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455)

也有 For input string: ""、For input string: ".11E.111E" 这类,偶尔还会静默返回错误的年份。

Reproduce(复现代码)

共享一个静态实例,多线程同时 parse:

public class Broken {
    private static final SimpleDateFormat FORMAT =
        new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) {
        try (ExecutorService pool = Executors.newFixedThreadPool(16)) {
            for (int i = 0; i < 1000; i++) {
                pool.submit(() -> {
                    Date d = FORMAT.parse("2026-03-14");
                    if (!"2026-03-14".equals(FORMAT.format(d))) {
                        System.out.println("wrong: " + FORMAT.format(d));
                    }
                    return null;
                });
            }
        }
    }
}

典型输出里会冒出 wrong: 0002-01-14、wrong: 2226-03-14 和散落的 NumberFormatException。单线程跑永远正常,多核机器上却时好时坏。

Root Cause(根因分析)

SimpleDateFormat 继承了 DateFormat,内部持有一个 Calendar 字段,作为格式化和解析时的「草稿纸」。解析时它按位把年、月、日写进这个 Calendar,最后再读出来组装结果。

关键在于这份草稿是共享的。线程 A 刚把年份设成 2026,线程 B 插进来把年份覆盖成自己输入里的 2226,线程 A 再回头读日字段,拼出来的就是一个「A 的年 + B 的日」的荒谬组合。没有任何单次写入是坏的,坏的是交叉写入,所以错误既静默又随机。

NumberFormatException: multiple points 同源:解析过程中维护了一个内部游标,两个线程交错推进让游标和缓冲区错位,数字解析就炸了。format 用的是同一个 Calendar,也一样不安全;parse 和 format 混在一个实例上更糟。

The Fix(解决方案)

新代码和能改的代码,直接用 java.time:DateTimeFormatter 不可变、线程安全,一个 static final 实例全应用共用,既无同步也无每次分配的额外开销:

private static final DateTimeFormatter FORMAT =
    DateTimeFormatter.ofPattern("uuuu-MM-dd");

LocalDate parsed = LocalDate.parse("2026-03-14", FORMAT);
String text = FORMAT.format(parsed);

存量代码不能换类型时,三种降级方式按成本递进:

每次调用新建实例,最简单也最正确:

private static String format(Date date) {
    return new SimpleDateFormat("yyyy-MM-dd").format(date);
}

或者用 ThreadLocal 每个线程一份,但要注意它在容器线程池里「实例和线程同寿命」,容易间接造成 classloader 泄漏,用完记得 remove();在 Java 21 虚拟线程下每个任务一个实例,反而不划算。

总之,SimpleDateFormat 唯一「错」的用法就是让它 static 共享,绕开共享这一步,所有诡异现象都消失。

References(参考来源)

最后更新于 2026-09-09

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法