SimpleDateFormat为什么是线程不安全的?

2024-02-20 09:04

本文主要是介绍SimpleDateFormat为什么是线程不安全的?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

目录

      • 在日常开发中,Date工具类使用频率相对较高,大家通常都会这样写:
      • 这很简单啊,有什么争议吗?
      • 格式化后出现的时间错乱。
      • 看看Java 8是如何解决时区问题的:
      • 在处理带时区的国际化时间问题,推荐使用jdk8的日期时间类:
      • 在与前端联调时,报了个错,```java.lang.NumberFormatException: multiple points```,起初我以为是时间格式传的不对,仔细一看,不对啊。
      • 看一下```SimpleDateFormat.parse```的源码:

大家好,我是哪吒。

在日常开发中,Date工具类使用频率相对较高,大家通常都会这样写:

public static Date getData(String date) throws ParseException {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");return sdf.parse(date);
}public static Date getDataByFormat(String date, String format) throws ParseException {SimpleDateFormat sdf = new SimpleDateFormat(format);return sdf.parse(date);
}

这很简单啊,有什么争议吗?

你应该听过“时区”这个名词,大家也都知道,相同时刻不同时区的时间是不一样的。

因此在使用时间时,一定要给出时区信息。

public static void getDataByZone(String param, String format) throws ParseException {SimpleDateFormat sdf = new SimpleDateFormat(format);// 默认时区解析时间表示Date date = sdf.parse(param);System.out.println(date + ":" + date.getTime());// 东京时区解析时间表示sdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));Date newYorkDate = sdf.parse(param);System.out.println(newYorkDate + ":" + newYorkDate.getTime());
}public static void main(String[] args) throws ParseException {getDataByZone("2023-11-10 10:00:00","yyyy-MM-dd HH:mm:ss");
}

对于当前的上海时区和纽约时区,转化为 UTC 时间戳是不同的时间。

对于同一个本地时间的表示,不同时区的人解析得到的 UTC 时间一定是不同的,反过来不同的本地时间可能对应同一个 UTC。

在这里插入图片描述

格式化后出现的时间错乱。

public static void getDataByZoneFormat(String param, String format) throws ParseException {SimpleDateFormat sdf = new SimpleDateFormat(format);Date date = sdf.parse(param);// 默认时区格式化输出System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date));// 东京时区格式化输出TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date));
}public static void main(String[] args) throws ParseException {getDataByZoneFormat("2023-11-10 10:00:00","yyyy-MM-dd HH:mm:ss");
}

我当前时区的 Offset(时差)是 +8 小时,对于 +9 小时的纽约,整整差了1个小时,北京早上 10 点对应早上东京 11 点。

在这里插入图片描述

看看Java 8是如何解决时区问题的:

Java 8 推出了新的时间日期类 ZoneId、ZoneOffset、LocalDateTime、ZonedDateTime 和 DateTimeFormatter,处理时区问题更简单清晰。

public static void getDataByZoneFormat8(String param, String format) throws ParseException {ZoneId zone = ZoneId.of("Asia/Shanghai");ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");ZoneId timeZone = ZoneOffset.ofHours(2);// 格式化器DateTimeFormatter dtf = DateTimeFormatter.ofPattern(format);ZonedDateTime date = ZonedDateTime.of(LocalDateTime.parse(param, dtf), zone);// withZone设置时区DateTimeFormatter dtfz = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z");System.out.println(dtfz.withZone(zone).format(date));System.out.println(dtfz.withZone(tokyoZone).format(date));System.out.println(dtfz.withZone(timeZone).format(date));
}public static void main(String[] args) throws ParseException {getDataByZoneFormat8("2023-11-10 10:00:00","yyyy-MM-dd HH:mm:ss");
}
  • Asia/Shanghai对应+8,对应2023-11-10 10:00:00;
  • Asia/Tokyo对应+9,对应2023-11-10 11:00:00;
  • timeZone 是+2,所以对应2023-11-10 04:00:00;

在这里插入图片描述

在处理带时区的国际化时间问题,推荐使用jdk8的日期时间类:

  1. 通过ZoneId,定义时区;
  2. 使用ZonedDateTime保存时间;
  3. 通过withZone对DateTimeFormatter设置时区;
  4. 进行时间格式化得到本地时间;

思路比较清晰,不容易出错。

在与前端联调时,报了个错,java.lang.NumberFormatException: multiple points,起初我以为是时间格式传的不对,仔细一看,不对啊。

百度一下,才知道是高并发情况下SimpleDateFormat有线程安全的问题。

下面通过模拟高并发,把这个问题复现一下:

public static void getDataByThread(String param, String format) throws InterruptedException {ExecutorService threadPool = Executors.newFixedThreadPool(5);SimpleDateFormat sdf = new SimpleDateFormat(format);// 模拟并发环境,开启5个并发线程for (int i = 0; i < 5; i++) {threadPool.execute(() -> {for (int j = 0; j < 2; j++) {try {System.out.println(sdf.parse(param));} catch (ParseException e) {System.out.println(e);}}});}threadPool.shutdown();threadPool.awaitTermination(1, TimeUnit.HOURS);
}

果不其然,报错。还将2023年转换成2220年,我勒个乖乖。

在时间工具类里,时间格式化,我都是这样弄的啊,没问题啊,为啥这个不行?原来是因为共用了同一个SimpleDateFormat,在工具类里,一个线程一个SimpleDateFormat,当然没问题啦!

在这里插入图片描述

可以通过TreadLocal 局部变量,解决SimpleDateFormat的线程安全问题。

public static void getDataByThreadLocal(String time, String format) throws InterruptedException {ExecutorService threadPool = Executors.newFixedThreadPool(5);ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>() {@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat(format);}};// 模拟并发环境,开启5个并发线程for (int i = 0; i < 5; i++) {threadPool.execute(() -> {for (int j = 0; j < 2; j++) {try {System.out.println(sdf.get().parse(time));} catch (ParseException e) {System.out.println(e);}}});}threadPool.shutdown();threadPool.awaitTermination(1, TimeUnit.HOURS);
}

在这里插入图片描述

看一下SimpleDateFormat.parse的源码:

public class SimpleDateFormat extends DateFormat {@Overridepublic Date parse(String text, ParsePosition pos){CalendarBuilder calb = new CalendarBuilder();Date parsedDate;try {parsedDate = calb.establish(calendar).getTime();// If the year value is ambiguous,// then the two-digit year == the default start yearif (ambiguousYear[0]) {if (parsedDate.before(defaultCenturyStart)) {parsedDate = calb.addYear(100).establish(calendar).getTime();}}}}
}class CalendarBuilder {Calendar establish(Calendar cal) {boolean weekDate = isSet(WEEK_YEAR)&& field[WEEK_YEAR] > field[YEAR];if (weekDate && !cal.isWeekDateSupported()) {// Use YEAR insteadif (!isSet(YEAR)) {set(YEAR, field[MAX_FIELD + WEEK_YEAR]);}weekDate = false;}cal.clear();// Set the fields from the min stamp to the max stamp so that// the field resolution works in the Calendar.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;}}}...}
}
  1. 先new CalendarBuilder();
  2. 通过parsedDate = calb.establish(calendar).getTime();解析时间;
  3. establish方法内先cal.clear(),再重新构建cal,整个操作没有加锁;

上面几步就会导致在高并发场景下,线程1正在操作一个Calendar,此时线程2又来了。线程1还没来得及处理 Calendar 就被线程2清空了。

因此,通过编写Date工具类,一个线程一个SimpleDateFormat,还是有一定道理的。


🏆哪吒多年工作总结:Java学习路线总结,搬砖工逆袭Java架构师

华为OD机试 2023B卷题库疯狂收录中,刷题点这里

刷的越多,抽中的概率越大,每一题都有详细的答题思路、详细的代码注释、样例测试,发现新题目,随时更新,全天CSDN在线答疑。

这篇关于SimpleDateFormat为什么是线程不安全的?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

线程池ThreadPoolExecutor应用过程

《线程池ThreadPoolExecutor应用过程》:本文主要介绍如何使用ThreadPoolExecutor创建线程池,包括其构造方法、常用方法、参数校验以及如何选择合适的拒绝策略,文章还讨论... 目录ThreadPoolExecutor构造说明及常用方法为什么强制要求使用ThreadPoolExec

Java线程池核心参数原理及使用指南

《Java线程池核心参数原理及使用指南》本文详细介绍了Java线程池的基本概念、核心类、核心参数、工作原理、常见类型以及最佳实践,通过理解每个参数的含义和工作原理,可以更好地配置线程池,提高系统性能,... 目录一、线程池概述1.1 什么是线程池1.2 线程池的优势二、线程池核心类三、ThreadPoolE

input的accept属性让文件上传安全高效

《input的accept属性让文件上传安全高效》文章介绍了HTML的input文件上传`accept`属性在文件上传校验中的重要性和优势,通过使用`accept`属性,可以减少前端JavaScrip... 目录前言那个悄悄毁掉你上传体验的“常见写法”改变一切的 html 小特性:accept真正的魔法:让

JAVA线程的周期及调度机制详解

《JAVA线程的周期及调度机制详解》Java线程的生命周期包括NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING和TERMINATED,线程调度依赖操作系统,采用抢占... 目录Java线程的生命周期线程状态转换示例代码JAVA线程调度机制优先级设置示例注意事项JAVA线程

Redis的安全机制详细介绍及配置方法

《Redis的安全机制详细介绍及配置方法》本文介绍Redis安全机制的配置方法,包括绑定IP地址、设置密码、保护模式、禁用危险命令、防火墙限制、TLS加密、客户端连接限制、最大内存使用和日志审计等,通... 目录1. 绑定 IP 地址2. 设置密码3. 保护模式4. 禁用危险命令5. 通过防火墙限制访问6.

深入理解Redis线程模型的原理及使用

《深入理解Redis线程模型的原理及使用》Redis的线程模型整体还是多线程的,只是后台执行指令的核心线程是单线程的,整个线程模型可以理解为还是以单线程为主,基于这种单线程为主的线程模型,不同客户端的... 目录1 Redis是单线程www.chinasem.cn还是多线程2 Redis如何保证指令原子性2.

C++实现一个简易线程池的使用小结

《C++实现一个简易线程池的使用小结》在现代软件开发中,多线程编程已经成为提升程序性能的常见手段,本文主要介绍了C++实现一个简易线程池的使用小结,感兴趣的可以了解一下... 在现代软件开发中,多线程编程已经成为提升程序性能的常见手段。无论是处理大量 I/O 请求的服务器,还是进行 CPU 密集型计算的应用

JDK21对虚拟线程的几种用法实践指南

《JDK21对虚拟线程的几种用法实践指南》虚拟线程是Java中的一种轻量级线程,由JVM管理,特别适合于I/O密集型任务,:本文主要介绍JDK21对虚拟线程的几种用法,文中通过代码介绍的非常详细,... 目录一、参考官方文档二、什么是虚拟线程三、几种用法1、Thread.ofVirtual().start(

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

Java 线程池+分布式实现代码

《Java线程池+分布式实现代码》在Java开发中,池通过预先创建并管理一定数量的资源,避免频繁创建和销毁资源带来的性能开销,从而提高系统效率,:本文主要介绍Java线程池+分布式实现代码,需要... 目录1. 线程池1.1 自定义线程池实现1.1.1 线程池核心1.1.2 代码示例1.2 总结流程2. J