news 2026/6/9 22:51:12

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/6/9 20:11:00

历史照片内容解析:老照片中的人物与场景还原

历史照片内容解析&#xff1a;老照片中的人物与场景还原 引言&#xff1a;让尘封影像“开口说话” 泛黄的边角、模糊的轮廓、低分辨率的成像——这些是历史老照片的典型特征。它们承载着家族记忆、社会变迁与时代风貌&#xff0c;却因缺乏文字标注或背景信息而难以被系统化理解…

作者头像 李华
网站建设 2026/6/9 20:09:56

铁路运维保障:轨道异物入侵AI报警系统建设

铁路运维保障&#xff1a;轨道异物入侵AI报警系统建设核心价值&#xff1a;通过集成阿里开源的“万物识别-中文-通用领域”模型&#xff0c;构建一套轻量、高效、可落地的轨道异物入侵AI检测系统&#xff0c;实现对铁路沿线非法闯入物体&#xff08;如行人、车辆、动物、障碍物…

作者头像 李华
网站建设 2026/6/9 20:09:53

30分钟搭建个人国外中文新闻聚合站

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 使用Python Flask框架快速搭建一个新闻聚合网站原型。网站应能自动从3-5个预定义的国外中文新闻网站RSS源获取内容&#xff0c;并在首页展示新闻列表。包含简单的分类功能&#xf…

作者头像 李华
网站建设 2026/6/9 20:10:13

AI识别故障排除:预置环境中的调试技巧

AI识别故障排除&#xff1a;预置环境中的调试技巧 作为一名技术支持工程师&#xff0c;你是否经常遇到这样的困扰&#xff1a;客户反馈AI识别系统出现问题&#xff0c;但由于环境差异、依赖版本不一致等原因&#xff0c;你很难在本地复现这些问题&#xff1f;本文将介绍如何利用…

作者头像 李华
网站建设 2026/6/9 4:36:09

汇编语言全接触-74.用Soft-ICE实现源码级调试

在 Windows 编程中&#xff0c;Soft-ICE 是一件必不可少的调试工具&#xff0c;但 Windows 程序的编程中有很多的时候是和数据结构和 API 打交道&#xff0c;经过编译以后&#xff0c;原来的 mov stRect.left,eax 之类的代码也就变成了 mov [xxxx],eax&#xff0c;invoke Updat…

作者头像 李华
网站建设 2026/6/9 4:36:06

AI如何用unplugin-auto-import自动优化前端依赖导入

快速体验 打开 InsCode(快马)平台 https://www.inscode.net输入框内输入如下内容&#xff1a; 创建一个Vue3项目&#xff0c;集成unplugin-auto-import插件&#xff0c;配置自动导入Vue相关API&#xff08;如ref, reactive&#xff09;、Element Plus组件和自定义工具函数。要…

作者头像 李华