java中SimpleDateFormat 的多线程安全问题

 更新时间:2026年02月09日 09:22:31   作者:思静鱼  
本文主要介绍了java中SimpleDateFormat 的多线程安全问题,包括其内部可变状态和竞争条件,文章提供了四种解决方案,下面就来详细的介绍一下

SimpleDateFormat 的多线程安全问题。这是生产环境中一个非常经典且危险的问题,因为它可能不会立即导致程序崩溃,而是 silently(静默地)产生错误的数据,极难排查。

问题根源:可变状态与竞争条件

SimpleDateFormat 不是线程安全的,其根本原因在于它内部维护了可变的、共享的状态(一个 Calendar 对象),并且没有使用同步机制来保护这个状态。

  1. 内部状态:当你调用 parse 或 format 方法时,SimpleDateFormat 会使用其内部的 Calendar 对象来执行计算。
  2. 竞争条件:
    • 线程 A 调用 parse("2023-10-25"),开始解析,将日期值设置到内部的 Calendar 中。
    • 在 线程 A 完成解析并返回结果之前,线程 B 也调用了 parse("2024-11-30"),并清空/覆盖了内部 Calendar 的状态。
    • 此时,线程 A 继续执行,从被 线程 B 污染了的 Calendar 中读取值,最终返回一个错误的 Date 对象(可能是 “2024-10-25” 或其他混乱的结果)。
    • 更糟的情况下,可能会直接抛出 NumberFormatException、ArrayIndexOutOfBoundsException 等异常。

问题复现示例

下面的代码清晰地展示了这个问题:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CountDownLatch;

public class SimpleDateFormatThreadSafetyDemo {

    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws InterruptedException {
        int threadCount = 10;
        CountDownLatch latch = new CountDownLatch(threadCount);
        String dateString = "2023-10-25 15:30:00";

        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                try {
                    // 所有线程都尝试解析同一个字符串
                    Date date = sdf.parse(dateString);
                    // 打印解析结果和当前线程,如果线程不安全,结果会五花八门
                    System.out.println(Thread.currentThread().getName() + " - Parsed date: " + date);
                } catch (ParseException e) {
                    System.out.println(Thread.currentThread().getName() + " - Parse failed: " + e.getMessage());
                } catch (NumberFormatException e) {
                    // 多线程下可能抛出的其他异常
                    System.out.println(Thread.currentThread().getName() + " - NumberFormatException: " + e.getMessage());
                } finally {
                    latch.countDown();
                }
            }).start();
        }

        latch.await(); // 等待所有线程结束
        System.out.println("All threads finished.");
    }
}

运行结果可能如下(每次运行都可能不同):

Thread-2 - Parsed date: Wed Oct 25 15:30:00 CST 2023 // 正确
Thread-4 - Parsed date: Mon Nov 30 15:30:00 CST 2026 // 完全错误!
Thread-0 - Parse failed: For input string: ""
Thread-1 - NumberFormatException: multiple points
Thread-3 - Parsed date: Wed Oct 25 15:30:00 CST 2023 // 正确
...

你可以看到,在并发访问下,出现了:

  1. 解析出完全错误的日期。
  2. 抛出 ParseException。
  3. 抛出其他运行时异常,如 NumberFormatException。

解决方案

有几种常见的方法来解决这个多线程安全问题。

方案 1:局部变量(每次创建新实例)【推荐用于低并发】

最简单的方法是在每个需要使用的的方法内部创建新的 SimpleDateFormat 实例。

public Date parseDate(String dateString) throws ParseException {
    // 每次调用都创建一个新的 SimpleDateFormat,线程私有,绝对安全
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    return sdf.parse(dateString);
}

优点:简单直观,绝对线程安全。
缺点:如果方法被高频调用,会创建大量临时对象,增加 GC 压力,性能较差。

方案 2:使用synchronized加锁

将共享的 SimpleDateFormat 实例的访问用 synchronized 块保护起来。

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

    public static Date parse(String dateString) throws ParseException {
        synchronized (sdf) { // 使用类对象或sdf对象作为锁
            return sdf.parse(dateString);
        }
    }

    public static String format(Date date) {
        synchronized (sdf) {
            return sdf.format(date);
        }
    }
}

优点:避免了对象的频繁创建,复用了实例。
缺点:在高并发场景下,锁竞争会成为性能瓶颈。

方案 3:使用ThreadLocal【最佳推荐】

这是兼顾性能线程安全的最佳方案。它为每个线程提供一份独立的 SimpleDateFormat 实例副本,从而避免了竞争。

public class ThreadSafeDateFormatter {

    private static final ThreadLocal<SimpleDateFormat> threadLocalDateFormat =
            ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static Date parse(String dateString) throws ParseException {
        // get() 方法会返回当前线程独有的 SimpleDateFormat 实例
        return threadLocalDateFormat.get().parse(dateString);
    }

    public static String format(Date date) {
        return threadLocalDateFormat.get().format(date);
    }

    // 重要!如果使用线程池,在线程任务结束时最好清理 ThreadLocal,防止内存泄漏
    public static void remove() {
        threadLocalDateFormat.remove();
    }
}

优点

  • 线程安全,每个线程有自己的副本,无竞争。
  • 高性能,避免了频繁创建实例和锁竞争。
    缺点
  • 使用稍复杂。
  • 需要注意内存泄漏问题(特别是在使用线程池时),在使用完毕后调用 remove() 方法。

方案 4:切换到 Java 8 的java.time包【终极方案】

Java 8 开始,引入了全新的日期时间 API (java.time 包)。这些类(如 LocalDateTime, DateTimeFormatter) 是 不可变且线程安全的

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class Java8DateUtils {
    // DateTimeFormatter 是线程安全的,可以放心定义为常量
    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static LocalDateTime parse(String dateString) {
        return LocalDateTime.parse(dateString, formatter); // 线程安全
    }

    public static String format(LocalDateTime dateTime) {
        return formatter.format(dateTime); // 线程安全
    }
}

这是现代 Java 开发的首选方案
优点

  • 绝对线程安全。
  • API 设计更清晰、更强大。
  • 是 Java 官方的未来方向。

总结

方案线程安全性能推荐度
局部变量安全差(对象创建开销)⭐⭐⭐ (简单场景)
synchronized安全中(有锁竞争)⭐⭐ ( legacy code )
ThreadLocal安全⭐⭐⭐⭐ (维护旧项目时)
Java 8 DateTimeFormatter安全⭐⭐⭐⭐⭐ (新项目必选)

最终建议:

  • 如果是新项目,请毫不犹豫地使用 Java 8 的 java.time API。
  • 如果必须维护使用 SimpleDateFormat 的旧代码,请使用 ThreadLocal 方案来修复多线程问题。

到此这篇关于java中SimpleDateFormat 的多线程安全问题的文章就介绍到这了,更多相关java SimpleDateFormat多线程安全内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Springboot启动执行特定代码的方式汇总

    Springboot启动执行特定代码的方式汇总

    这篇文章主要介绍了Springboot启动执行特定代码的几种方式,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-12-12
  • springboot中请求路径配置在配置文件中详解

    springboot中请求路径配置在配置文件中详解

    这篇文章主要介绍了springboot中请求路径配置在配置文件中,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • java去除数组重复元素的四种方法

    java去除数组重复元素的四种方法

    本文给大家分享四种java去除数组重复元素的方法,每种方法通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2021-11-11
  • Java 使用POI生成带联动下拉框的excel表格实例代码

    Java 使用POI生成带联动下拉框的excel表格实例代码

    本文通过实例代码给大家分享Java 使用POI生成带联动下拉框的excel表格,代码简单易懂,非常不错,具有参考借鉴价值,需要的朋友参考下吧
    2017-09-09
  • SpringBoot实现微服务通信的多种方式

    SpringBoot实现微服务通信的多种方式

    微服务通信是指在分布式系统中,各个微服务之间进行数据交互和通信的过程,今天我们将探讨在Spring Boot中实现微服务通信的多种方式,文章通过代码示例给大家介绍的非常详细,需要的朋友可以参考下
    2024-07-07
  • JAVA之读取properties时路径的注意问题

    JAVA之读取properties时路径的注意问题

    这篇文章主要介绍了JAVA之读取properties时路径的注意问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-08-08
  • Java中的Semaphore信号量使用方法代码实例

    Java中的Semaphore信号量使用方法代码实例

    这篇文章主要介绍了Java中的Semaphore信号量使用方法代码实例,Semaphore是一种基于计数的信号量,它可以设定一个阈值,基于此,多个线程竞争获取许可信号,做自己的申请后归还,超过阈值后,线程申请许可信号将会被阻塞,需要的朋友可以参考下
    2023-11-11
  • Java使用pdfbox实现给pdf文件加图片水印

    Java使用pdfbox实现给pdf文件加图片水印

    有时候需要给pdf加水印,市面上工具都是收费的要会员,还是自食其力吧;尝试过 spire.pdf.free 那个超过10页就不行了!所以本文还是使用了pdfbox,感兴趣的可以了解一下
    2022-11-11
  • Java时间轮调度算法的代码实现

    Java时间轮调度算法的代码实现

    时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂度和资源开销,本文给大家介绍了Java时间轮调度算法的代码实现,需要的朋友可以参考下
    2025-03-03
  • Spring Bean实例化实现过程解析

    Spring Bean实例化实现过程解析

    这篇文章主要介绍了Spring Bean实例化实现过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02

最新评论