news 2026/7/14 23:29:57

SimpleDateFormat 线程安全问题详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SimpleDateFormat 线程安全问题详解

1、重现 SimpleDateFormat 类的线程安全问题

面试中常提到 SimpleDateFormat 线程不安全,为了重现这个问题,可以使用线程池结合 CountDownLatch 和 Semaphore 类。

示例代码

java

package com.batch.controller; import java.text.SimpleDateFormat; import java.util.concurrent.*; /** * @Author: zouming * @Date: 2024/5/29 0:26 */ public class SimpleDateFormatDemo { // 执行总次数 private static final int EXECUTE_COUNT = 1000; // 同时运行的线程数量 private static final int THREAD_COUNT = 20; // SimpleDateFormat 对象(共享,线程不安全) private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); try { for (int i = 0; i < EXECUTE_COUNT; i++) { executorService.execute(() -> { try { semaphore.acquire(); // 核心方法:多个线程共享同一个 SimpleDateFormat 对象 simpleDateFormat.parse("2024-05-29"); } catch (InterruptedException e) { System.err.println("线程被中断:" + Thread.currentThread().getName()); } catch (Exception e) { System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); } finally { semaphore.release(); countDownLatch.countDown(); } }); } countDownLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("主程序线程被中断"); } finally { executorService.shutdown(); if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("线程池未能在规定时间内关闭"); } System.out.println("所有线程格式化日期成功"); } } }

运行结果

运行上述代码会抛出多种异常:

text

Exception in thread "pool-1-thread-4" Exception in thread "pool-1-thread-1" Exception in thread "pool-1-thread-2" 线程:pool-1-thread-7 格式化日期失败 线程:pool-1-thread-9 格式化日期失败 线程:pool-1-thread-10 格式化日期失败 Exception in thread "pool-1-thread-3" Exception in thread "pool-1-thread-5" Exception in thread "pool-1-thread-6" 线程:pool-1-thread-15 格式化日期失败 线程:pool-1-thread-21 格式化日期失败 Exception in thread "pool-1-thread-23" 线程:pool-1-thread-16 格式化日期失败 线程:pool-1-thread-11 格式化日期失败 java.lang.ArrayIndexOutOfBoundsException 线程:pool-1-thread-27 格式化日期失败 at java.lang.System.arraycopy(Native Method) at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:597) at java.lang.StringBuffer.append(StringBuffer.java:367) at java.text.DigitList.getLong(DigitList.java:191) 线程:pool-1-thread-25 格式化日期失败 at java.text.DecimalFormat.parse(DecimalFormat.java:2084) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) 线程:pool-1-thread-14 格式化日期失败 at java.text.DateFormat.parse(DateFormat.java:364) at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47) 线程:pool-1-thread-13 格式化日期失败 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) java.lang.NumberFormatException: For input string: "" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) 线程:pool-1-thread-20 格式化日期失败 at java.lang.Long.parseLong(Long.java:601) at java.lang.Long.parseLong(Long.java:631) at java.text.DigitList.getLong(DigitList.java:195) at java.text.DecimalFormat.parse(DecimalFormat.java:2084) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364) at io.binghe.concurrent.lab06.SimpleDateFormatTest01.lambda$main$0(SimpleDateFormatTest01.java:47) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)

结论

在高并发下使用 SimpleDateFormat 类格式化日期时抛出了异常,证明 SimpleDateFormat 类不是线程安全的!

2、SimpleDateFormat 类为何不是线程安全的

SimpleDateFormat 继承自 DateFormat 类,DateFormat 类中维护了一个全局的 Calendar 变量:

java

public abstract class DateFormat extends Format { /** * The {@link Calendar} instance used for calculating the date-time fields * and the instant of time. This field is used for both formatting and * parsing. * * <p>Subclasses should initialize this field to a {@link Calendar} * appropriate for the {@link Locale} associated with this * {@code DateFormat}. * @serial */ protected Calendar calendar; // 省略其他代码... }

从注释可以看出,这个 Calendar 对象既用于格式化也用于解析日期时间。

在 SimpleDateFormat 的parse()方法中,会调用CalendarBuilder.establish()方法,该方法中会先调用cal.clear()清除 Calendar 对象中设置的值,再调用cal.set()重新设置新的值:

java

Calendar establish(Calendar cal) { // ... 省略部分代码 cal.clear(); // 先清除 // ... 设置新值 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; } } } // ... 省略部分代码 return cal; }

由于 Calendar 内部并没有线程安全机制,并且clear()set()操作也都不是原子性的,所以当多个线程同时操作一个 SimpleDateFormat 时就会引起 Calendar 的值混乱。类似地,format()方法也存在同样的问题。

因此,SimpleDateFormat 类不是线程安全的根本原因是:DateFormat 类中的 Calendar 对象被多线程共享,而 Calendar 对象本身不支持线程安全。

3、解决 SimpleDateFormat 类的线程安全问题

方案1:局部变量法

java

public class SimpleDateFormatDemo01 { // 执行总次数 private static final int EXECUTE_COUNT = 1000; // 同时运行的线程数量 private static final int THREAD_COUNT = 20; public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); try { for (int i = 0; i < EXECUTE_COUNT; i++) { executorService.execute(() -> { try { semaphore.acquire(); // 每次使用时创建新的 SimpleDateFormat 对象 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); simpleDateFormat.parse("2024-05-29"); } catch (InterruptedException e) { System.err.println("线程被中断:" + Thread.currentThread().getName()); } catch (Exception e) { System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); } finally { semaphore.release(); countDownLatch.countDown(); } }); } countDownLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("主程序线程被中断"); } finally { executorService.shutdown(); if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("线程池未能在规定时间内关闭"); } System.out.println("所有线程格式化日期成功"); } } }

缺点:在高并发下会创建大量的 SimpleDateFormat 类对象,影响程序的性能。

方案2:synchronized 锁方式

java

public class SimpleDateFormatDemo02 { // 执行总次数 private static final int EXECUTE_COUNT = 1000; // 同时运行的线程数量 private static final int THREAD_COUNT = 20; // SimpleDateFormat 对象 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); try { for (int i = 0; i < EXECUTE_COUNT; i++) { executorService.execute(() -> { try { semaphore.acquire(); // 使用 synchronized 同步 synchronized (simpleDateFormat) { simpleDateFormat.parse("2020-01-01"); } } catch (InterruptedException e) { System.err.println("线程被中断:" + Thread.currentThread().getName()); } catch (Exception e) { System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); } finally { semaphore.release(); countDownLatch.countDown(); } }); } countDownLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("主程序线程被中断"); } finally { executorService.shutdown(); if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("线程池未能在规定时间内关闭"); } System.out.println("所有线程格式化日期成功"); } } }

缺点:同一时刻只能有一个线程执行parse()方法,影响程序的执行性能。

方案3:Lock 锁方式

java

import java.text.SimpleDateFormat; import java.util.concurrent.*; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class SimpleDateFormatDemo03 { // 执行总次数 private static final int EXECUTE_COUNT = 1000; // 同时运行的线程数量 private static final int THREAD_COUNT = 20; // SimpleDateFormat 对象 private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd"); // Lock 对象 private static Lock lock = new ReentrantLock(); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); try { for (int i = 0; i < EXECUTE_COUNT; i++) { executorService.execute(() -> { try { semaphore.acquire(); lock.lock(); // 加锁 simpleDateFormat.parse("2020-01-01"); } catch (InterruptedException e) { System.err.println("线程被中断:" + Thread.currentThread().getName()); } catch (Exception e) { System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); } finally { semaphore.release(); countDownLatch.countDown(); lock.unlock(); // 释放锁 } }); } countDownLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("主程序线程被中断"); } finally { executorService.shutdown(); if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("线程池未能在规定时间内关闭"); } System.out.println("所有线程格式化日期成功"); } } }

缺点:同样会影响高并发场景下的性能。

方案4:ThreadLocal 方式

java

import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.concurrent.*; public class SimpleDateFormatDemo04 { // 执行总次数 private static final int EXECUTE_COUNT = 1000; // 同时运行的线程数量 private static final int THREAD_COUNT = 20; // 使用 ThreadLocal private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } }; public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); try { for (int i = 0; i < EXECUTE_COUNT; i++) { executorService.execute(() -> { try { semaphore.acquire(); // 从 ThreadLocal 获取 SimpleDateFormat threadLocal.get().parse("2020-01-01"); } catch (InterruptedException e) { System.err.println("线程被中断:" + Thread.currentThread().getName()); } catch (Exception e) { System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); } finally { semaphore.release(); countDownLatch.countDown(); } }); } countDownLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("主程序线程被中断"); } finally { executorService.shutdown(); if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("线程池未能在规定时间内关闭"); } threadLocal.remove(); // 清理 ThreadLocal System.out.println("所有线程格式化日期成功"); } } }

优点:每个线程使用自己的 SimpleDateFormat 副本,线程安全且性能较好。

方案5:DateTimeFormatter 方式(Java 8+)

java

import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.concurrent.*; public class SimpleDateFormatDemo05 { // 执行总次数 private static final int EXECUTE_COUNT = 1000; // 同时运行的线程数量 private static final int THREAD_COUNT = 20; // DateTimeFormatter(线程安全) private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); try { for (int i = 0; i < EXECUTE_COUNT; i++) { executorService.execute(() -> { try { semaphore.acquire(); // 使用 DateTimeFormatter LocalDate.parse("2020-01-01", formatter); } catch (InterruptedException e) { System.err.println("线程被中断:" + Thread.currentThread().getName()); } catch (Exception e) { System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); } finally { semaphore.release(); countDownLatch.countDown(); } }); } countDownLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("主程序线程被中断"); } finally { executorService.shutdown(); if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("线程池未能在规定时间内关闭"); } System.out.println("所有线程格式化日期成功"); } } }

优点:DateTimeFormatter 是线程安全的,推荐在 Java 8+ 中使用。

方案6:joda-time 方式

首先添加依赖:

xml

<dependency> <groupId>joda-time</groupId> <artifactId>joda-time</artifactId> <version>2.9.9</version> </dependency>

示例代码:

java

import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import java.util.concurrent.*; public class SimpleDateFormatDemo06 { // 执行总次数 private static final int EXECUTE_COUNT = 1000; // 同时运行的线程数量 private static final int THREAD_COUNT = 20; // DateTimeFormatter private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyy-MM-dd"); public static void main(String[] args) throws InterruptedException { final Semaphore semaphore = new Semaphore(THREAD_COUNT); final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT); ExecutorService executorService = Executors.newCachedThreadPool(); try { for (int i = 0; i < EXECUTE_COUNT; i++) { executorService.execute(() -> { try { semaphore.acquire(); DateTime.parse("2020-01-01", dateTimeFormatter).toDate(); } catch (InterruptedException e) { System.err.println("线程被中断:" + Thread.currentThread().getName()); } catch (Exception e) { System.err.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败"); e.printStackTrace(); } finally { semaphore.release(); countDownLatch.countDown(); } }); } countDownLatch.await(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); System.err.println("主程序线程被中断"); } finally { executorService.shutdown(); if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { System.err.println("线程池未能在规定时间内关闭"); } System.out.println("所有线程格式化日期成功"); } } }

优点:joda-time 是线程安全的第三方库,性能经过高并发考验。

4、总结

解决方案原理优点缺点推荐度
局部变量法每次使用创建新对象简单直接创建大量对象,性能差不推荐
synchronized锁同步访问共享对象线程安全并发性能差不推荐
Lock锁显式锁控制访问更灵活的锁控制并发性能差不推荐
ThreadLocal每个线程独立副本线程安全,性能好需要管理内存推荐
DateTimeFormatterJava 8 线程安全类官方推荐,性能好仅限 Java 8+强烈推荐
joda-time第三方线程安全库性能好,功能丰富需要额外依赖推荐

最佳实践建议:

  1. Java 8+ 项目:优先使用DateTimeFormatter

  2. Java 8 以下项目

    • 高并发场景:使用ThreadLocaljoda-time

    • 低并发场景:可使用局部变量法

  3. 性能要求高joda-timeThreadLocal

  4. 代码简洁性DateTimeFormatter(Java 8+)

注意:SimpleDateFormat 的线程安全问题主要体现在其内部共享的 Calendar 对象上,因此在多线程环境下必须采取适当的同步措施或使用线程安全的替代方案。

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

小微企业如何用免费网站提升线上存在感?

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 生成一个小微企业展示网站&#xff0c;包含&#xff1a;1.轮播展示3款主打产品 2.产品详情页 3.在线留言表单 4.企业联系方式 5.简易后台管理系统。要求使用React框架&#xff0c;…

作者头像 李华
网站建设 2026/7/14 19:21:51

数字艺术家的秘密武器:5步搞定AI绘画+万物识别联合作业流

数字艺术家的秘密武器&#xff1a;5步搞定AI绘画万物识别联合作业流 作为一名概念设计师&#xff0c;你是否遇到过这样的困扰&#xff1a;用Stable Diffusion生成的精美作品&#xff0c;需要手动为每个元素添加标签&#xff0c;工作量巨大&#xff1f;更糟的是&#xff0c;当你…

作者头像 李华
网站建设 2026/7/14 19:22:10

零基础入门:5分钟学会编写李跳跳规则

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个交互式李跳跳规则学习平台&#xff0c;包含&#xff1a;1)规则语法图解教程 2)实时演练沙盒环境 3)常见错误自动检测 4)渐进式难度案例库。采用引导式教学&#xff0c;用户…

作者头像 李华
网站建设 2026/7/14 19:21:16

电子制造检测:PCB板焊接质量AI判定系统

电子制造检测&#xff1a;PCB板焊接质量AI判定系统 引言&#xff1a;从人工质检到智能视觉的工业升级 在现代电子制造产线中&#xff0c;PCB&#xff08;印刷电路板&#xff09;焊接质量检测是决定产品良率的关键环节。传统依赖人工目检的方式存在效率低、标准不一、漏检率高…

作者头像 李华
网站建设 2026/7/14 19:21:21

AI识物全攻略:从环境搭建到模型调优一站式教程

AI识物全攻略&#xff1a;从环境搭建到模型调优一站式教程 在图像识别项目中&#xff0c;环境配置往往是让开发者头疼的第一道门槛。无论是识别动植物、日常物品还是特殊场景&#xff0c;一个标准化的部署方案能大幅提升开发效率。本文将带你从零开始&#xff0c;使用预置环境镜…

作者头像 李华
网站建设 2026/7/14 20:24:25

气象云图分类:识别积雨云、卷云等典型云系

气象云图分类&#xff1a;识别积雨云、卷云等典型云系 引言&#xff1a;从通用图像识别到专业气象分析的跨越 在人工智能视觉领域&#xff0c;万物识别-中文-通用领域模型的出现标志着AI对现实世界理解能力的一次重大跃迁。这类模型不仅能够识别日常物体&#xff0c;还能通过迁…

作者头像 李华