SimpleDateFormat在多线程下的安全问题及解决

 更新时间:2026年03月07日 14:58:59   作者:zmbwcx2003  
SimpleDateFormat类在Java中用于日期时间的格式化和解析,但在高并发环境下存在线程不安全的问题,原因是它共享了一个Calendar对象,导致多个线程操作时出现冲突,为了解决这个问题,可以使用局部变量、加锁或使用线程安全的DateTimeFormatter类

情景重现

SimpleDateFormat类是Java开发中的一个日期时间的转化类。它可以满足绝大多数的开发场景,但是在高并发下会出现并发问题。

接下来查看下文中的案例。

public class TestSimpleDateFormat {
    public static void main(String[] args) {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                try {
                    Date parse = format.parse("2003-01-01");
                    System.out.println(parse);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

上面代码简单来说就是创建了一个SimpleDateFormat类对象,该对象被后续会被五个线程使用,去转化日期格式并打印。

我们来查看输出结果:

java.lang.NumberFormatException: empty String
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at java.text.DigitList.getDouble(DigitList.java:169)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at test.lambda$main$0(test.java:21)
    at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at java.text.DigitList.getDouble(DigitList.java:169)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at test.lambda$main$0(test.java:21)
    at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at java.text.DigitList.getDouble(DigitList.java:169)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at test.lambda$main$0(test.java:21)
    at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: empty String
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at java.text.DigitList.getDouble(DigitList.java:169)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at test.lambda$main$0(test.java:21)
    at java.lang.Thread.run(Thread.java:745)
Fri Nov 01 00:00:00 CST 2222

可以看到,只输出了一次时间转化,并且该输出格式还是错误的。

接下来我们来查看为什么SimpleDateFormat类是线程不安全的。

SimpleDateFormat解析

我们根据parse()方法,查看SimpleDateFormat是如何进行格式转换的。

我们可以看到,返回结果是根据另一个方法获取到的,接下来我们接着查看该parse()源码。

这是一个抽象方法,接着我们去查看它的具体实现。

可以看到该方法很长,但是我们只关注返回如何结果,直接拉到最后查看该方法如何返回一个日期格式 。上图中,最后一次修改parseDate对象是在箭头的位置。那么我们查看getTime()方法。

可以看到该方法是由Calendar类提供的,该类名翻译为中文就是日历的意思,并且返回结果也是我们需要的日期格式,那么我们就可以确定该方法用于给parseDate对象提供返回值的,接下来回退一下查看其他方法哪个是提供Calendar对象来调用getTime()方法的。

现在我们清楚了Calendar类对象是由establish()方法提供的了,该方法中需要一个参数calendar对象。该对象由SimpleDateFormat的父类DateFormat来维护。

此时我们或许大概明白是因为SimpleDateFormat类之所以线程不安全的问题是因为在多线程下共享了calendar对象。接下来我们继续查看establish()方法,验证是否是这样,下面是具体源码

    Calendar establish(Calendar cal) {
        boolean weekDate = isSet(WEEK_YEAR)
                       && field[WEEK_YEAR] > field[YEAR];
        if (weekDate && !cal.isWeekDateSupported()) {
            // Use YEAR instead
            if (!isSet(YEAR)) {
                set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
            }
            weekDate = false;
        }

        cal.clear();
        // Set the fields from the min stamp to the max stamp so that
        // the field resolution works in the Calendar.
        for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
            for (int index = 0; index <= maxFieldIndex; index++) {
                if (field[index] == stamp) {
                    cal.set(index, field[MAX_FIELD + index]);
                    break;
                }
            }
        }

        if (weekDate) {
            int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
            int dayOfWeek = isSet(DAY_OF_WEEK) ?
                                field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
            if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
                if (dayOfWeek >= 8) {
                    dayOfWeek--;
                    weekOfYear += dayOfWeek / 7;
                    dayOfWeek = (dayOfWeek % 7) + 1;
                } else {
                    while (dayOfWeek <= 0) {
                        dayOfWeek += 7;
                        weekOfYear--;
                    }
                }
                dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
            }
            cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);
        }
        return cal;
    }

可以看到,在该方法中,对cal对象执行了clear()方法与set()方法,我们查看clear方法是做什么的

该方法提供了类似初始化的功能,将上一次的格式转化保存的cal属性清除。

而set()方法将本次的格式转换需要的数据更新。因此,我们可以确定了SimpleDateFormat类之所以线程不安全就是因为共享了calendar对象。

解决方案

为了避免SimpleDateFormat格式转换带来的并发问题,我们可以采取以下几个措施

局部变量

我们已经知道了产生线程安全问题的原因是共享了相同属性,那么我们只要让每个线程都包含自己的属性就可以避免该问题的发生。

具体实现代码如下:

public class TestSimpleDateFormat {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
        new Thread(()->{
            try {
                    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
                    Date parse = format.parse("2003-01-01");
                    System.out.println(parse);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

运行结果如下:

Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003

这种方式不太推荐,因为会创建大量的SimpleDateFormat对象,占用内存空间。 

加锁

除了让每个线程都拥有自己独立的对象外,我们也可以保证在同一时刻下,只有一个线程对共享属性进行修改,那就是加锁。具体实现代码如下

public class TestSimpleDateFormat{
    public static void main(String[] args) {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    Date parse;
                    synchronized (format){
                        parse = format.parse("2003-01-01");
                    }
                    System.out.println(parse);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

但是这种方法不太推荐,因为能够出现格式转化错误的情况已经是很大的并发了,如果还使用同步锁的话会影响性能。

使用线程变量

public class test {
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd");
        }
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    Date parse = threadLocal.get().parse("2023-01-01");
                    System.out.println(parse);
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

使用DateTimeFormatter

在JDK8之后提供了线程安全的格式转化DateTimeFormatter类,使用方法如下

public class test {
    public static void main(String[] args) {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    TemporalAccessor parse = dateTimeFormatter.parse("2003-06-03");
                        System.out.println(parse);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

输出结果为:

{},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Spring Boot缓存实战 Caffeine示例

    Spring Boot缓存实战 Caffeine示例

    本篇文章主要介绍了Spring Boot缓存实战 Caffeine示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-02-02
  • 从原理到实战深入理解Java 断言assert

    从原理到实战深入理解Java 断言assert

    本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代,附常见问题解答及实践案例,感兴趣的朋友一起看看吧
    2025-06-06
  • 一文详解如何使用Java获取PDF页面信息

    一文详解如何使用Java获取PDF页面信息

    了解 PDF 页面属性是我们在处理文档、内容提取、打印设置或页面重组等任务时不可或缺的一环,下面我们就来看看如何使用Java语言获取这些信息吧
    2025-07-07
  • Java的LinkedHashSet源码深入讲解

    Java的LinkedHashSet源码深入讲解

    这篇文章主要介绍了Java的LinkedHashSet源码深入讲解,LinkedHashSet是HashSet的子类,而由于HashSet实现了Set接口,因此LinkedHashSet也间接实现了Set类,LinkedHashSet类属于java.base模块,java.util包下,需要的朋友可以参考下
    2023-09-09
  • SpringMVC源码解析之消息转换器HttpMessageConverter

    SpringMVC源码解析之消息转换器HttpMessageConverter

    本篇文章主要介绍了SpringMVC源码解析之消息转换器HttpMessageConverter ,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-11-11
  • SpringSecurity当中的CSRF防范使用详解

    SpringSecurity当中的CSRF防范使用详解

    这篇文章主要介绍了SpringSecurity当中的CSRF防范使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-05-05
  • 在IDEA里Git特别慢的问题及解决过程

    在IDEA里Git特别慢的问题及解决过程

    IDEA中Git操作慢的问题可以通过重命名`runnerw.exe`和`runnerw64.exe`文件来解决,这可能是由于杀毒软件或防火墙拦截导致的
    2025-12-12
  • 基于HTML5+js+Java实现单文件文件上传到服务器功能

    基于HTML5+js+Java实现单文件文件上传到服务器功能

    应公司要求,在HTML5页面上实现上传文件到服务器功能,对于我这样的菜鸟,真是把我难住了,最后还是请教大神搞定的,下面小编把例子分享到脚本之家平台,供大家参考
    2017-08-08
  • java腾讯AI人脸对比对接代码实例

    java腾讯AI人脸对比对接代码实例

    这篇文章主要介绍了java腾讯AI人脸对比对接,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-03-03
  • java.lang.Runtime.exec() Payload知识点详解

    java.lang.Runtime.exec() Payload知识点详解

    在本篇文章里小编给大家整理的是一篇关于java.lang.Runtime.exec() Payload知识点相关内容,有兴趣的朋友们学习下。
    2020-03-03

最新评论