SimpleDateFormat在多线程下的安全问题

目录

情景重现

SimpleDateFormat解析

解决方案

局部变量

 加锁

 使用线程变量

使用DateTimeFormatter 


情景重现

SimpleDateFormat类是Java开发中的一个日期时间的转化类。它可以满足绝大多数的开发场景,但是在高并发下会出现并发问题。接下来查看下文中的案例。

public class TestSimpleDateFormat {public static void main(String[] args) {SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");for (int i = 0; i < 5; i++) {new Thread(()->{try {Date parse = format.parse("2003-01-01");System.out.println(parse);} catch (Exception e) {e.printStackTrace();}}).start();}}
}

上面代码简单来说就是创建了一个SimpleDateFormat类对象,该对象被后续会被五个线程使用,去转化日期格式并打印。我们来查看输出结果。

java.lang.NumberFormatException: empty Stringat sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)at java.lang.Double.parseDouble(Double.java:538)at java.text.DigitList.getDouble(DigitList.java:169)at java.text.DecimalFormat.parse(DecimalFormat.java:2056)at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)at java.text.DateFormat.parse(DateFormat.java:364)at test.lambda$main$0(test.java:21)at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: multiple pointsat sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)at java.lang.Double.parseDouble(Double.java:538)at java.text.DigitList.getDouble(DigitList.java:169)at java.text.DecimalFormat.parse(DecimalFormat.java:2056)at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)at java.text.DateFormat.parse(DateFormat.java:364)at test.lambda$main$0(test.java:21)at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: multiple pointsat sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)at java.lang.Double.parseDouble(Double.java:538)at java.text.DigitList.getDouble(DigitList.java:169)at java.text.DecimalFormat.parse(DecimalFormat.java:2056)at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)at java.text.DateFormat.parse(DateFormat.java:364)at test.lambda$main$0(test.java:21)at java.lang.Thread.run(Thread.java:745)
java.lang.NumberFormatException: empty Stringat sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)at java.lang.Double.parseDouble(Double.java:538)at java.text.DigitList.getDouble(DigitList.java:169)at java.text.DecimalFormat.parse(DecimalFormat.java:2056)at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)at java.text.DateFormat.parse(DateFormat.java:364)at test.lambda$main$0(test.java:21)at java.lang.Thread.run(Thread.java:745)
Fri Nov 01 00:00:00 CST 2222

可以看到,只输出了一次时间转化,并且该输出格式还是错误的。接下来我们来查看为什么SimpleDateFormat类是线程不安全的。

SimpleDateFormat解析

我们根据parse()方法,查看SimpleDateFormat是如何进行格式转换的。

我们可以看到,返回结果是根据另一个方法获取到的,接下来我们接着查看该parse()源码。

 这是一个抽象方法,接着我们去查看它的具体实现。

可以看到该方法很长,但是我们只关注返回如何结果,直接拉到最后查看该方法如何返回一个日期格式 。上图中,最后一次修改parseDate对象是在箭头的位置。那么我们查看getTime()方法。

可以看到该方法是由Calendar类提供的,该类名翻译为中文就是日历的意思,并且返回结果也是我们需要的日期格式,那么我们就可以确定该方法用于给parseDate对象提供返回值的,接下来回退一下查看其他方法哪个是提供Calendar对象来调用getTime()方法的。

现在我们清楚了Calendar类对象是由establish()方法提供的了,该方法中需要一个参数calendar对象。该对象由SimpleDateFormat的父类DateFormat来维护。

此时我们或许大概明白是因为SimpleDateFormat类之所以线程不安全的问题是因为在多线程下共享了calendar对象。接下来我们继续查看establish()方法,验证是否是这样,下面是具体源码

    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;}}}if (weekDate) {int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;int dayOfWeek = isSet(DAY_OF_WEEK) ?field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {if (dayOfWeek >= 8) {dayOfWeek--;weekOfYear += dayOfWeek / 7;dayOfWeek = (dayOfWeek % 7) + 1;} else {while (dayOfWeek <= 0) {dayOfWeek += 7;weekOfYear--;}}dayOfWeek = toCalendarDayOfWeek(dayOfWeek);}cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);}return cal;}

可以看到,在该方法中,对cal对象执行了clear()方法与set()方法,我们查看clear方法是做什么的

该方法提供了类似初始化的功能,将上一次的格式转化保存的cal属性清除。

而set()方法将本次的格式转换需要的数据更新。因此,我们可以确定了SimpleDateFormat类之所以线程不安全就是因为共享了calendar对象。

解决方案

为了避免SimpleDateFormat格式转换带来的并发问题,我们可以采取以下几个措施

局部变量

我们已经知道了产生线程安全问题的原因是共享了相同属性,那么我们只要让每个线程都包含自己的属性就可以避免该问题的发生。具体实现代码如下

public class TestSimpleDateFormat {public static void main(String[] args) {for (int i = 0; i < 5; i++) {new Thread(()->{try {SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");Date parse = format.parse("2003-01-01");System.out.println(parse);} catch (Exception e) {e.printStackTrace();}}).start();}}
}

 运行结果如下

Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003
Wed Jan 01 00:00:00 CST 2003

这种方式不太推荐,因为会创建大量的SimpleDateFormat对象,占用内存空间。 

 加锁

除了让每个线程都拥有自己独立的对象外,我们也可以保证在同一时刻下,只有一个线程对共享属性进行修改,那就是加锁。具体实现代码如下

public class TestSimpleDateFormat{public static void main(String[] args) {SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");for (int i = 0; i < 5; i++) {new Thread(() -> {try {Date parse;synchronized (format){parse = format.parse("2003-01-01");}System.out.println(parse);} catch (Exception e) {e.printStackTrace();}}).start();}}
}

但是这种方法不太推荐,因为能够出现格式转化错误的情况已经是很大的并发了,如果还使用同步锁的话会影响性能。

 使用线程变量

public class test {private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {@Overrideprotected DateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");}};public static void main(String[] args) {for (int i = 0; i < 5; i++) {new Thread(() -> {try {Date parse = threadLocal.get().parse("2023-01-01");System.out.println(parse);} catch (ParseException e) {e.printStackTrace();}}).start();}}
}

使用DateTimeFormatter 

在JDK8之后提供了线程安全的格式转化DateTimeFormatter类,使用方法如下

public class test {public static void main(String[] args) {DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");for (int i = 0; i < 5; i++) {new Thread(() -> {try {TemporalAccessor parse = dateTimeFormatter.parse("2003-06-03");System.out.println(parse);} catch (Exception e) {e.printStackTrace();}}).start();}}
}

输出结果为

{},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03
{},ISO resolved to 2003-06-03

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/225760.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unity中Shader的BRDF解析(一)

文章目录 前言现在我们主要来看Standard的 漫反射 和 镜面反射一、PBS的核心计算BRDF二、Standard的镜面高光颜色三、具体的BRDF计算对于BRDF的具体计算&#xff0c;在下篇文章中&#xff0c;继续解析 四、最终代码.cginc文件Shader文件 前言 在上篇文章中&#xff0c;我们解析…

当消费增值模式遇上Dapp:擦出创新商业的火花

随着区块链技术和智能合约的不断发展&#xff0c;去中心化应用&#xff08;Dapp&#xff09;逐渐成为一种创新的商业模式。当消费增值模式与Dapp相遇&#xff0c;它们之间擦出了怎样的火花呢&#xff1f; 一、Dapp与消费增值模式的结合 Dapp是一种基于区块链技术和智能合约的去…

在Rust中处理命令行参数和环境变量

1.摘要 Rust的命令行和环境变量处理在标准库中提供了一整套实现方法, 在本文中除了探索标准库的使用方法之外, 也在不断适应Rust独有的语法特点。在本文中, 我们通过标准库函数的返回值熟悉了迭代器的使用方法, 操作迭代器精确控制保存的内容, 包括字符串和键值对的使用方法。…

Mysql快速查找用逗号分割的列中含有某个字符的行:FIND_IN_SET

看标题比较绕口&#xff0c;但是我举一个例子你就清楚了 这是一个查询&#xff1a;我现在想要的是attr_val中含有黑色属性的行&#xff1a; 你用模糊匹配可以&#xff0c;用REGEXP也行&#xff0c;下面介绍一种mysql自带的函数&#xff1a;FIND_IN_SET。它的用法是&#xff1a…

每日汇评:原油价格正在等待欧佩克对2024年供应削减配额的决定

OPEC会议推迟至周四&#xff0c;个别配额和供应削减仍然是会议的核心议题&#xff1b; 原油价格在欧佩克会议前持平&#xff0c;但是否有意外的看涨取决于欧佩克的减产&#xff1b; 布伦特原油价格在关键的82美元和200均线的交叉点被明显拒绝后走低&#xff1b; 上周三&#xf…

JOSEF 漏电继电器JHOK-ZBL1 DH-50L 系统1140V 电源AC220V

系列型号&#xff1a; JHOK-ZBL多档切换式漏电&#xff08;剩余&#xff09;继电器 JHOK-ZBL1多档切换式漏电&#xff08;剩余&#xff09;继电器 JHOK-ZBL2多档切换式漏电&#xff08;剩余&#xff09;继电器 JHOK-ZBM多档切换式漏电&#xff08;剩余&#xff09;继电器 …

【傻瓜级JS-DLL-WINCC-PLC交互】3.JS-DLL进行交互

思路 JS-DLL-WINCC-PLC之间进行交互&#xff0c;思路&#xff0c;先用Visual Studio创建一个C#的DLL控件&#xff0c;然后这个控件里面嵌入浏览器组件&#xff0c;实现JS与DLL通信&#xff0c;然后DLL放入到WINCC里面的图形编辑器中&#xff0c;实现DLL与WINCC的通信。然后PLC与…

玻色量子事件活动

2023年 2023.7 玻色量子携最新相干光量子计算机惊艳亮相2023数字经济大会 2023.6 打造“新型计算数据中心”&#xff01;玻色量子与科华数据&#xff08;002335.SZ&#xff09;携手共创 2023.6 玻色量子“天工量子大脑”亮相中关村论坛&#xff0c;大放异彩 2023.5 100量…

机器学习实战第3天:手写数字识别

☁️主页 Nowl &#x1f525;专栏《机器学习实战》 《机器学习》 &#x1f4d1;君子坐而论道&#xff0c;少年起而行之 ​ 文章目录 一、任务描述 二、数据集描述 三、主要代码 &#xff08;1&#xff09;主要代码库的说明与导入方法 &#xff08;2&#xff09;数据预…

实现了父类 纯虚函数为什么还有 无法解析外部符号错误

使用背景&#xff1a; 将C 的函数或接口使用 pybind11 封装成可以供python 使用调用的接口或函数&#xff0c;使用了CMake 编译&#xff08;若之前可以编译通过&#xff0c;现在编译不通过&#xff0c;重新选择 source code 路径&#xff09;成 VS 2019 可使用的目标解决方案&a…

四丶openlayer之瓦片地图

瓦片地图源于一种大地图解决方案&#xff0c;针对一整块非常大的地图进行切片&#xff0c;分成很多相同大小的小块地图&#xff0c;在用户访问的时候&#xff0c;再一块一块小地图加载&#xff0c;拼接在一起&#xff0c;从而还原成一整块大的地图。这样做的优点在于&#xff0…

zblog插件-zblog采集插件下载

在当今数字化的时代&#xff0c;博客已经成为人们分享思想、经验和知识的重要平台。而对于使用zblog博客系统的用户来说&#xff0c;充实博客内容是提高用户体验和吸引读者的不二法门。然而&#xff0c;手动收集内容对于博主来说既费时又繁琐。在这个背景下&#xff0c;zblog插…