news 2026/8/5 10:42:15

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/26 1:54:30

Hunyuan-MT-7B-WEBUI多语言SEO内容批量生成

Hunyuan-MT-7B-WEBUI&#xff1a;多语言SEO内容批量生成的工程化实践 在跨境电商、全球化内容运营和数字营销日益依赖自动化生产的今天&#xff0c;如何高效生成高质量的多语言SEO内容&#xff0c;已成为企业提升国际竞争力的关键命题。传统依赖人工翻译或商业API的方式&#…

作者头像 李华
网站建设 2026/7/31 1:29:24

如何用AI快速解析MSDN文档并生成代码示例

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个AI工具&#xff0c;能够自动解析MSDN ITELLYOU官网的Windows API文档。要求&#xff1a;1. 输入MSDN文档URL后自动抓取内容 2. 识别文档中的函数声明、参数说明和返回值 3…

作者头像 李华
网站建设 2026/7/26 14:41:37

小白必看:轻松理解并修复brew命令报错

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 设计一个面向新手的可视化指导工具&#xff0c;用最简化的方式解决brew命令问题。要求&#xff1a;1. 使用通俗易懂的语言解释错误原因&#xff1b;2. 提供图形化界面引导操作&…

作者头像 李华
网站建设 2026/8/4 11:02:25

不用写代码!快马平台一键生成ORACLE列转行方案

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 开发一个无需编码的ORACLE列转行原型生成器。功能&#xff1a;1.上传Excel样本数据 2.自动识别转换需求 3.生成完整SQL脚本 4.提供测试数据 5.一键部署到ORACLE。使用Kimi-K2模型&…

作者头像 李华
网站建设 2026/7/26 1:57:22

UNOCSS零基础入门:10分钟创建你的第一个项目

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 开发一个UNOCSS新手教学项目&#xff0c;包含&#xff1a;1.基础配置说明 2.5个常用工具类示例 3.简单卡片组件实现 4.交互式学习练习区 5.常见问题解答。要求代码注释详细&#x…

作者头像 李华
网站建设 2026/7/26 17:20:33

2026年国内企业AI大模型选型与海外模型接入实战指南

&#x1f4da; 开篇&#xff1a;AI选型的核心命题的是“适配”而非“跟风” 随着AI大模型技术的快速迭代&#xff0c;“百模齐放”已从趋势演变为常态。海外阵营中&#xff0c;OpenAI、Google凭借技术积淀筑牢通用能力护城河&#xff1b;国内市场上&#xff0c;百度、阿里、字…

作者头像 李华