news 2026/4/23 6:08:34

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/3/13 20:21:57

AI如何帮你自动生成JAVA注解代码?

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 请生成一个完整的JAVA项目&#xff0c;包含以下功能&#xff1a;1. 使用Spring Boot框架&#xff1b;2. 包含常见的Controller、Service、Repository层注解&#xff1b;3. 实现一个…

作者头像 李华
网站建设 2026/4/16 17:48:39

AI如何优化10000GDCN在线测速工具的开发

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 开发一个10000GDCN在线测速工具&#xff0c;要求使用AI自动生成前端界面和后端逻辑。前端应包括测速按钮、实时速度显示图表和历史记录功能。后端需要实现网络请求测速算法&#x…

作者头像 李华
网站建设 2026/4/20 5:50:02

十分钟搞定:用云端GPU训练你的第一个中文识别模型

十分钟搞定&#xff1a;用云端GPU训练你的第一个中文识别模型 作为一名刚接触深度学习的编程爱好者&#xff0c;你是否遇到过这样的困扰&#xff1a;想训练一个简单的图像识别模型&#xff0c;但在自己的笔记本电脑上跑一次训练就要耗费一整天&#xff1f;更让人头疼的是&#…

作者头像 李华
网站建设 2026/4/23 11:22:33

AI助力React开发:自动生成组件代码与逻辑

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 请生成一个React函数组件&#xff0c;实现一个可折叠的FAQ列表。要求&#xff1a;1. 使用useState管理展开/折叠状态 2. 接受questions数组作为props&#xff0c;格式为{id, quest…

作者头像 李华
网站建设 2026/4/23 13:04:07

Notepad++ vs 其他编辑器:为什么它依然是开发者的首选

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 设计一个Notepad插件&#xff0c;用于对比不同文本编辑器的性能。插件可以记录和显示编辑器的启动时间、内存占用、文件加载速度等数据&#xff0c;并生成可视化报告。支持自定义测…

作者头像 李华
网站建设 2026/4/19 20:00:14

企业级应用:CWRSYNC在跨地域数据同步中的实践

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个企业级CWRSYNC应用案例演示&#xff1a;1.模拟跨国企业北京-纽约-伦敦三地办公场景2.实现实时文件同步&#xff08;延迟<1秒&#xff09;3.包含权限管理系统4.展示带宽…

作者头像 李华