还在用 SimpleDateFormat 做时间格式化?小心项目崩掉!

2023-10-15 10:10

本文主要是介绍还在用 SimpleDateFormat 做时间格式化?小心项目崩掉!,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Hollis的新书限时折扣中,一本深入讲解Java基础的干货笔记!

来源:blog.csdn.net/QiuHaoqian/article/details/116594422

今天聊聊 SimpleDateFormat 在多线程环境下存在线程安全问题。

1 SimpleDateFormat.parse() 方法的线程安全问题

1.1 错误示例

错误使用SimpleDateFormat.parse()的代码如下:

import java.text.SimpleDateFormat;public class SimpleDateFormatTest {private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static void main(String[] args) {/*** SimpleDateFormat线程不安全,没有保证线程安全(没有加锁)的情况下,禁止使用全局SimpleDateFormat,否则报错 NumberFormatException** private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");*/for (int i = 0; i < 20; ++i) {Thread thread = new Thread(() -> {try {// 错误写法会导致线程安全问题System.out.println(Thread.currentThread().getName() + "--" + SIMPLE_DATE_FORMAT.parse("2020-06-01 11:35:00"));} catch (Exception e) {e.printStackTrace();}}, "Thread-" + i);thread.start();}}
}

报错:

44127261a7b40b90aa521633cc2a86f4.png

1.2 非线程安全原因分析

查看源码中可以看到:SimpleDateFormat继承DateFormat类,SimpleDateFormat转换日期是通过继承自DateFormat类的Calendar对象来操作的,Calendar对象会被用来进行日期-时间计算,既被用于format方法也被用于parse方法。

0cdab0848e63b4192970c9db00671e06.png

SimpleDateFormat 的 parse(String source) 方法 会调用继承自父类的 DateFormat 的 parse(String source) 方法

4bddd1773e91a4bd704427cb2311e7e7.png

DateFormat 的 parse(String source) 方法会调用SimpleDateFormat中重写的 parse(String text, ParsePosition pos) 方法,该方法中有个地方需要关注

bba5da482a9b4f3c037f9f496ad7c9f1.png

SimpleDateFormat 中重写的 parse(String text, ParsePosition pos) 方法中调用了 establish(calendar) 这个方法:

651dd5acb2a84ed497d0dbeb6306fa11.png

该方法中调用了 Calendar 的 clear() 方法

e18b258118fa0246388183c90b10f424.png

可以发现整个过程中Calendar对象它并不是线程安全的,如果,a线程将calendar清空了,calendar 就没有新值了,恰好此时b线程刚好进入到parse方法用到了calendar对象,那就会产生线程安全问题了!

正常情况下:

83be0bee39ac15a10a5c55a3f54dcdaf.png

非线程安全的流程:

283354d39c4dbe41dfc4f06c7485eba0.png

1.3 解决方法

方法1:每个线程都new一个SimpleDateFormat

import java.text.SimpleDateFormat;public class SimpleDateFormatTest {public static void main(String[] args) {for (int i = 0; i < 20; ++i) {Thread thread = new Thread(() -> {try {// 每个线程都new一个SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");System.out.println(Thread.currentThread().getName() + "--" + simpleDateFormat.parse("2020-06-01 11:35:00"));} catch (Exception e) {e.printStackTrace();}}, "Thread-" + i);thread.start();}}
}

方式2:synchronized等方式加锁

public class SimpleDateFormatTest {private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static void main(String[] args) {for (int i = 0; i < 20; ++i) {Thread thread = new Thread(() -> {try {synchronized (SIMPLE_DATE_FORMAT) {System.out.println(Thread.currentThread().getName() + "--" + SIMPLE_DATE_FORMAT.parse("2020-06-01 11:35:00"));}} catch (Exception e) {e.printStackTrace();}}, "Thread-" + i);thread.start();}}
}

方式3:使用ThreadLocal 为每个线程创建一个独立变量

import java.text.DateFormat;
import java.text.SimpleDateFormat;public class SimpleDateFormatTest {private static final ThreadLocal<DateFormat> SAFE_SIMPLE_DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));public static void main(String[] args) {for (int i = 0; i < 20; ++i) {Thread thread = new Thread(() -> {try {System.out.println(Thread.currentThread().getName() + "--" + SAFE_SIMPLE_DATE_FORMAT.get().parse("2020-06-01 11:35:00"));} catch (Exception e) {e.printStackTrace();}}, "Thread-" + i);thread.start();}}
}

2 SimpleDateFormat.format() 方法的线程安全问题

2.1 错误示例

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class SimpleDateFormatTest {// 时间格式化对象private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");public static void main(String[] args) throws InterruptedException {// 创建线程池执行任务ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));for (int i = 0; i < 1000; i++) {int finalI = i;// 执行任务threadPool.execute(new Runnable() {@Overridepublic void run() {Date date = new Date(finalI * 1000); // 得到时间对象formatAndPrint(date); // 执行时间格式化}});}threadPool.shutdown(); // 线程池执行完任务之后关闭}/*** 格式化并打印时间*/private static void formatAndPrint(Date date) {String result = simpleDateFormat.format(date); // 执行格式化System.out.println("时间:" + result); // 打印最终结果}
}

c19a46d1cda268b7bc1d5e7a78ba488b.png

从上述结果可以看出,程序的打印结果竟然有重复内容的,正确的情况应该是没有重复的时间才对。

2.2 非线程安全原因分析

为了找到问题所在,查看 SimpleDateFormat 中 format 方法的源码来排查一下问题,format 源码如下:

694bb739b696d1e090f80034e83b1b44.png

从上述源码可以看出,在执行 SimpleDateFormat.format() 方法时,会使用 calendar.setTime() 方法将输入的时间进行转换,那么我们想想一下这样的场景:

  • 线程 1 执行了 calendar.setTime(date) 方法,将用户输入的时间转换成了后面格式化时所需要的时间;

  • 线程 1 暂停执行,线程 2 得到 CPU 时间片开始执行;

  • 线程 2 执行了 calendar.setTime(date) 方法,对时间进行了修改;

  • 线程 2 暂停执行,线程 1 得出 CPU 时间片继续执行,因为线程 1 和线程 2 使用的是同一对象,而时间已经被线程 2 修改了,所以此时当线程 1 继续执行的时候就会出现线程安全的问题了。

正常的情况下,程序的执行是这样的:

fd4b4b4d54acf72efad44866bdfd1f0e.png

非线程安全的执行流程是这样的:

e15e04533459e282c851a434b5ed60f1.png

2.3 解决方法

同样有三种解决方法

方法1:每个线程都new一个SimpleDateFormat

public class SimpleDateFormatTest {public static void main(String[] args) throws InterruptedException {// 创建线程池执行任务ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));for (int i = 0; i < 1000; i++) {int finalI = i;// 执行任务threadPool.execute(new Runnable() {@Overridepublic void run() {// 得到时间对象Date date = new Date(finalI * 1000);// 执行时间格式化formatAndPrint(date);}});}// 线程池执行完任务之后关闭threadPool.shutdown();}/*** 格式化并打印时间*/private static void formatAndPrint(Date date) {String result = new SimpleDateFormat("mm:ss").format(date); // 执行格式化System.out.println("时间:" + result); // 打印最终结果}
}

方式2:synchronized等方式加锁

所有的线程必须排队执行某些业务才行,这样无形中就降低了程序的运行效率了

public class SimpleDateFormatTest {// 时间格式化对象private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");public static void main(String[] args) throws InterruptedException {// 创建线程池执行任务ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));for (int i = 0; i < 1000; i++) {int finalI = i;// 执行任务threadPool.execute(new Runnable() {@Overridepublic void run() {Date date = new Date(finalI * 1000); // 得到时间对象formatAndPrint(date); // 执行时间格式化}});}// 线程池执行完任务之后关闭threadPool.shutdown();}/*** 格式化并打印时间*/private static void formatAndPrint(Date date) {// 执行格式化String result = null;// 加锁synchronized (SimpleDateFormatTest.class) {result = simpleDateFormat.format(date);}// 打印最终结果System.out.println("时间:" + result);}
}

方式3:使用ThreadLocal 为每个线程创建一个独立变量

public class SimpleDateFormatTest {// 创建 ThreadLocal 并设置默认值private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));public static void main(String[] args) {// 创建线程池执行任务ThreadPoolExecutor threadPool = new ThreadPoolExecutor(10, 10, 60,TimeUnit.SECONDS, new LinkedBlockingQueue<>(1000));// 执行任务for (int i = 0; i < 1000; i++) {int finalI = i;// 执行任务threadPool.execute(() -> {Date date = new Date(finalI * 1000); // 得到时间对象formatAndPrint(date); // 执行时间格式化});}threadPool.shutdown(); // 线程池执行完任务之后关闭}/*** 格式化并打印时间*/private static void formatAndPrint(Date date) {String result = dateFormatThreadLocal.get().format(date); // 执行格式化System.out.println("时间:" + result);  // 打印最终结果}
}

我的新书《深入理解Java核心技术》已经上市了,上市后一直蝉联京东畅销榜中,目前正在6折优惠中,想要入手的朋友千万不要错过哦~长按二维码即可购买~

9852b353f58d809484dea7638dc4f97b.png

长按扫码享受6折优惠

往期推荐

12422939a0924c1cf89a26a8ed69d1c2.png

入职一家新公司,如何快速熟悉代码?


2be001c3661bbcba46a8cd197b06b805.png

揭秘:春晚微信红包,是如何抗住 100 亿次请求的?


556d1acdec6151952280f3d1b0309245.png

你还不明白如何解决分布式Session?看这篇就够了!


如果你喜欢本文,

请长按二维码,关注 Hollis.

08d1c08a8057a54614f05a71575d8bc5.png

转发至朋友圈,是对我最大的支持。

点个 在看 

喜欢是一种感觉

在看是一种支持

↘↘↘

这篇关于还在用 SimpleDateFormat 做时间格式化?小心项目崩掉!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/216986

相关文章

Java实现时间与字符串互相转换详解

《Java实现时间与字符串互相转换详解》这篇文章主要为大家详细介绍了Java中实现时间与字符串互相转换的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、日期格式化为字符串(一)使用预定义格式(二)自定义格式二、字符串解析为日期(一)解析ISO格式字符串(二)解析自定义

一文教你如何将maven项目转成web项目

《一文教你如何将maven项目转成web项目》在软件开发过程中,有时我们需要将一个普通的Maven项目转换为Web项目,以便能够部署到Web容器中运行,本文将详细介绍如何通过简单的步骤完成这一转换过程... 目录准备工作步骤一:修改​​pom.XML​​1.1 添加​​packaging​​标签1.2 添加

tomcat多实例部署的项目实践

《tomcat多实例部署的项目实践》Tomcat多实例是指在一台设备上运行多个Tomcat服务,这些Tomcat相互独立,本文主要介绍了tomcat多实例部署的项目实践,具有一定的参考价值,感兴趣的可... 目录1.创建项目目录,测试文China编程件2js.创建实例的安装目录3.准备实例的配置文件4.编辑实例的

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

《Java时间轮调度算法的代码实现》时间轮是一种高效的定时调度算法,主要用于管理延时任务或周期性任务,它通过一个环形数组(时间轮)和指针来实现,将大量定时任务分摊到固定的时间槽中,极大地降低了时间复杂... 目录1、简述2、时间轮的原理3. 时间轮的实现步骤3.1 定义时间槽3.2 定义时间轮3.3 使用时

springboot集成Deepseek4j的项目实践

《springboot集成Deepseek4j的项目实践》本文主要介绍了springboot集成Deepseek4j的项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价... 目录Deepseek4j快速开始Maven 依js赖基础配置基础使用示例1. 流式返回示例2. 进阶

SpringBoot项目启动报错"找不到或无法加载主类"的解决方法

《SpringBoot项目启动报错找不到或无法加载主类的解决方法》在使用IntelliJIDEA开发基于SpringBoot框架的Java程序时,可能会出现找不到或无法加载主类com.example.... 目录一、问题描述二、排查过程三、解决方案一、问题描述在使用 IntelliJ IDEA 开发基于

SpringBoot项目使用MDC给日志增加唯一标识的实现步骤

《SpringBoot项目使用MDC给日志增加唯一标识的实现步骤》本文介绍了如何在SpringBoot项目中使用MDC(MappedDiagnosticContext)为日志增加唯一标识,以便于日... 目录【Java】SpringBoot项目使用MDC给日志增加唯一标识,方便日志追踪1.日志效果2.实现步

Python如何获取域名的SSL证书信息和到期时间

《Python如何获取域名的SSL证书信息和到期时间》在当今互联网时代,SSL证书的重要性不言而喻,它不仅为用户提供了安全的连接,还能提高网站的搜索引擎排名,那我们怎么才能通过Python获取域名的S... 目录了解SSL证书的基本概念使用python库来抓取SSL证书信息安装必要的库编写获取SSL证书信息

Ubuntu中Nginx虚拟主机设置的项目实践

《Ubuntu中Nginx虚拟主机设置的项目实践》通过配置虚拟主机,可以在同一台服务器上运行多个独立的网站,本文主要介绍了Ubuntu中Nginx虚拟主机设置的项目实践,具有一定的参考价值,感兴趣的可... 目录简介安装 Nginx创建虚拟主机1. 创建网站目录2. 创建默认索引文件3. 配置 Nginx4

SpringBoot项目启动错误:找不到或无法加载主类的几种解决方法

《SpringBoot项目启动错误:找不到或无法加载主类的几种解决方法》本文主要介绍了SpringBoot项目启动错误:找不到或无法加载主类的几种解决方法,具有一定的参考价值,感兴趣的可以了解一下... 目录方法1:更改IDE配置方法2:在Eclipse中清理项目方法3:使用Maven命令行在开发Sprin