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

一、前言

日期的转换与格式化在项目中应该是比较常用的了,最近同事小刚出去面试实在是没想到被 SimpleDateFormat 给摆了一道…

面试官:项目中的日期转换怎么用的?SimpleDateFormat 用过吗?能说一下 SimpleDateFormat 线程安全问题吗,以及如何解决?

同事小刚:用过的,平时就是在全局定义一个 static 的 SimpleDateFormat,然后在业务处理方法(controller)中直接使用,至于线程安全… 这个… 倒是没遇到过线程安全问题。

哎,面试官的考察点真的是难以捉摸,吐槽归吐槽,一起来看看这个类吧。

二、概述

SimpleDateFormat 类主要负责日期的转换与格式化等操作,在多线程的环境中,使用此类容易造成数据转换及处理的不正确,因为 SimpleDateFormat 类并不是线程安全的,但在单线程环境下是没有问题的。

SimpleDateFormat 在类注释中也提醒大家不适用于多线程场景:

Date formats are not synchronized.
It is recommended to create separate format instances for each thread.
If multiple threads access a format concurrently, it must be synchronized
externally.日期格式不同步。
建议为每个线程创建单独的格式实例。 
如果多个线程同时访问一种格式,则必须在外部同步该格式。

来看看阿里巴巴 java 开发规范是怎么描述 SimpleDateFormat 的:

三、模拟线程安全问题

无码无真相,接下来我们创建一个线程来模拟 SimpleDateFormat 线程安全问题:

创建 MyThread.java 类:

public class MyThread extends Thread{private SimpleDateFormat simpleDateFormat;// 要转换的日期字符串private String dateString;public MyThread(SimpleDateFormat simpleDateFormat, String dateString){this.simpleDateFormat = simpleDateFormat;this.dateString = dateString;}@Overridepublic void run() {try {Date date = simpleDateFormat.parse(dateString);String newDate = simpleDateFormat.format(date).toString();if(!newDate.equals(dateString)){System.out.println("ThreadName=" + this.getName()+ " 报错了,日期字符串:" + dateString+ " 转换成的日期为:" + newDate);}}catch (ParseException e){e.printStackTrace();}}
}

创建执行类 Test.java 类:

public class Test {// 一般我们使用SimpleDateFormat的时候会把它定义为一个静态变量,避免频繁创建它的对象实例private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY-MM-dd");public static void main(String[] args) {String[] dateStringArray = new String[] { "2020-09-10", "2020-09-11", "2020-09-12", "2020-09-13", "2020-09-14"};MyThread[] myThreads = new MyThread[5];// 创建线程for (int i = 0; i < 5; i++) {myThreads[i] = new MyThread(simpleDateFormat, dateStringArray[i]);}// 启动线程for (int i = 0; i < 5; i++) {myThreads[i].start();}}
}

执行截图如下:

从控制台打印的结果来看,使用单例的 SimpleDateFormat 类在多线程的环境中处理日期转换,极易出现转换异常(java.lang.NumberFormatException:multiple points)以及转换错误的情况。

四、线程不安全的原因

这个时候就需要看看源码了,format() 格式转换方法:

// 成员变量 Calendar
protected Calendar calendar;private StringBuffer format(Date date, StringBuffer toAppendTo,FieldDelegate delegate) {// Convert input date to time field listcalendar.setTime(date);boolean useDateFormatSymbols = useDateFormatSymbols();for (int i = 0; i < compiledPattern.length; ) {int tag = compiledPattern[i] >>> 8;int count = compiledPattern[i++] & 0xff;if (count == 255) {count = compiledPattern[i++] << 16;count |= compiledPattern[i++];}switch (tag) {case TAG_QUOTE_ASCII_CHAR:toAppendTo.append((char)count);break;case TAG_QUOTE_CHARS:toAppendTo.append(compiledPattern, i, count);i += count;break;default:subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);break;}}return toAppendTo;
}

我们把重点放在 calendar ,这个 format 方法在执行过程中,会操作成员变量 calendar 来保存时间 calendar.setTime(date)

但由于在声明 SimpleDateFormat 的时候,使用的是 static 定义的,那么这个 SimpleDateFormat 就是一个共享变量,SimpleDateFormat 中的 calendar 也就可以被多个线程访问到,所以问题就出现了,举个例子:

假设线程 A 刚执行完 calendar.setTime(date) 语句,把时间设置为 2020-09-01,但线程还没执行完,线程 B 又执行了 calendar.setTime(date) 语句,把时间设置为 2020-09-02,这个时候就出现幻读了,线程 A 继续执行下去的时候,拿到的 calendar.getTime 得到的时间就是线程B改过之后的。

除了 format() 方法以外,SimpleDateFormat 的 parse 方法也有同样的问题。

至此,我们发现了 SimpleDateFormat 的弊端,所以为了解决这个问题就是不要把 SimpleDateFormat 当做一个共享变量来使用。

五、如何解决线程安全

1、每次使用就创建一个新的 SimpleDateFormat

创建全局工具类 DateUtils.java

public class DateUtils {public static Date parse(String formatPattern, String dateString) throws ParseException {return new SimpleDateFormat(formatPattern).parse(dateString);}public static String  format(String formatPattern, Date date){return new SimpleDateFormat(formatPattern).format(date);}
}

所有用到 SimpleDateFormat 的地方全部用 DateUtils 替换,然后看一下执行结果:

好家伙,异常+错误终于是没了,这种解决处理错误的原理就是创建了多个 SimpleDateFormat 类的实例,在需要用到的地方创建一个新的实例,就没有线程安全问题,不过也加重了创建对象的负担,会频繁地创建和销毁对象,效率较低。

2、synchronized 锁

synchronized 就不展开介绍了,不了解的小伙伴请移步 > synchronized的底层原理?

变更一下 DateUtils.java

public class DateUtils {private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static Date parse(String formatPattern, String dateString) throws ParseException {synchronized (simpleDateFormat){return simpleDateFormat.parse(dateString);}}public static String format(String formatPattern, Date date) {synchronized (simpleDateFormat){return simpleDateFormat.format(date);}}
}

简单粗暴,synchronized 往上一套也可以解决线程安全问题,缺点自然就是并发量大的时候会对性能有影响,因为使用了 synchronized 加锁后的多线程就相当于串行,线程阻塞,执行效率低。

3、ThreadLocal(最佳MVP)

ThreadLocal 是 java 里一种特殊的变量,ThreadLocal 提供了线程本地的实例,它与普通变量的区别在于,每个使用该线程变量的线程都会初始化一个完全独立的实例副本。

继续改造 DateUtils.java

public class DateUtils {private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){@Overrideprotected DateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");}};public static Date parse(String formatPattern, String dateString) throws ParseException {return threadLocal.get().parse(dateString);}public static String format(String formatPattern, Date date) {return threadLocal.get().format(date);}
}

ThreadLocal 可以确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象,那么就不会存在竞争问题。

如果项目中还在使用 SimpleDateFormat 的话,推荐这种写法,但这样就结束了吗?

显然不是…

六、项目中推荐的写法

上边提到的阿里巴巴 java 开发手册给出了说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。

日期转换,SimpleDateFormat 固然好用,但是现在我们已经有了更好地选择,Java 8 引入了新的日期时间 API,并引入了线程安全的日期类,一起来看看。

  • Instant:瞬时实例。
  • LocalDate:本地日期,不包含具体时间 例如:2014-01-14 可以用来记录生日、纪念日、加盟日等。
  • LocalTime:本地时间,不包含日期。
  • LocalDateTime:组合了日期和时间,但不包含时差和时区信息。
  • ZonedDateTime:最完整的日期时间,包含时区和相对UTC或格林威治的时差。

新API还引入了 ZoneOffSet 和 ZoneId 类,使得解决时区问题更为简便。

解析、格式化时间的 DateTimeFormatter 类也进行了全部重新设计。

例如,我们使用 LocalDate 代替 Date,使用 DateTimeFormatter 代替 SimpleDateFormat,如下所示:

// 当前日期和时间
String DateNow = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")); 
System.out.println(DateNow);

这样就避免了 SimpleDateFormat 的线程不安全问题啦。

此时的 DateUtils.java

public class DateUtils {public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");public static LocalDate parse(String dateString){return LocalDate.parse(dateString, DATE_TIME_FORMATTER);}public static String format(LocalDate target) {return target.format(DATE_TIME_FORMATTER);}
}

七、最后总结

SimpleDateFormart 线程不安全问题

SimpleDateFormart 继承自 DateFormart,在 DataFormat 类内部有一个 Calendar 对象引用,SimpleDateFormat 转换日期都是靠这个 Calendar 对象来操作的,比如 parse(String),format(date) 等类似的方法,Calendar 在用的时候是直接使用的,而且是改变了 Calendar 的值,这样情况在多线程下就会出现线程安全问题,如果 SimpleDateFormart 是静态的话,那么多个 thread 之间就会共享这个 SimpleDateFormart,同时也会共享这个 Calendar 引用,那么就出现数据赋值覆盖情况,也就是线程安全问题。(现在项目中用到日期转换,都是使用的 java 8 中的 LocalDate,或者 LocalDateTime,本质是这些类是不可变类,不可变一定程度上保证了线程安全)。

解决方式

在多线程下可以使用 ThreadLocal 修饰 SimpleDateFormart,ThreadLocal 可以确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象,那么就不会存在竞争问题。

项目中推荐的写法

java 8 中引入新的日期类 API,这些类是不可变的,且线程安全的。

以后面试官再问项目中怎么使用日期转换的,尽量就不要说 SimpleDateFormat 了。

博客园持续更新,欢迎关注,未来,我们一起成长。

本文首发于博客园:https://www.cnblogs.com/niceyoo/p/13672913.html

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

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

相关文章

【Python 学习_第2周_程序代码】金角大王培训第二周练习_购物车代码,将写的代码和老师代码比较,记录下收获...

培训第二周&#xff0c;课堂练习为编写一段购物车代码&#xff0c;需求描述如下&#xff1a; 1.提示用户输入薪水 2.用户输入薪水后&#xff0c;打印商品编号、内容及价格 3.提醒用户输入商品代码&#xff0c;若余额大于等于商品价格&#xff0c;可购买&#xff1b;若小于&…

ActiveMQ Cannot send, channel has already failed: tcp:127.0.0.1:8161

仅针对如下错误内容&#xff1a; Cannot send, channel has already failed: tcp://127.0.0.1:8161一种尝试解决&#xff0c;修改连接端口为 61616&#xff1a; tcp://127.0.0.1:61616在没有修改过 ActiveMQ 配置文件情况下&#xff0c;默认 tcp 端口为 61616&#xff0c;htt…

pip安装报错处理+PyPi源切换教程

一、pip安装出错类型 1.1 pip版本过旧导致不能安装 报错提示&#xff1a; You are using pip version 9.0.3, however version 10.0.1 is available. You should consider upgrading via the python -m pip install --upgrade pip comm and. 可通过以下命令升级pip python -m p…

面试官:说一下List排序方法

1. 前言 排序算是比较高频的面试题了&#xff0c;节前面试了的两家公司都有问到排序问题&#xff0c;整理后分享给大家&#xff08;文末见总结&#xff09;。 通常我们想到实现排序就是 Collections 工具类的 sort() 方法&#xff0c;而 sort() 方法有两种&#xff1a; 直接调…

python之路——内置函数和匿名函数

楔子 在讲新知识之前&#xff0c;我们先来复习复习函数的基础知识。 问&#xff1a;函数怎么调用&#xff1f; 函数名() 如果你们这么说。。。那你们就对了&#xff01;好了记住这个事儿别给忘记了&#xff0c;咱们继续谈下一话题。。。 来你们在自己的环境里打印一下自己的名字…

SpringBoot打包成Docker镜像

1. 本文环境 Maven&#xff1a;3.6.3 &#xff08;Maven配置参考&#xff09; SpringBoot version&#xff1a;2.3.4.RELEASE Docker version&#xff1a; 19.03.11 &#xff08;Docker搭建参考&#xff09; JDK version&#xff1a;1.8.0_221 &#xff08;JDK搭建参考&…

Redis分布式锁—SETNX+Lua脚本实现篇

前言 平时的工作中&#xff0c;由于生产环境中的项目是需要部署在多台服务器中的&#xff0c;所以经常会面临解决分布式场景下数据一致性的问题&#xff0c;那么就需要引入分布式锁来解决这一问题。 针对分布式锁的实现&#xff0c;目前比较常用的就如下几种方案&#xff1a;…

Windows10远程报错:由于CredSSP加密Oracle修正

https://support.microsoft.com/zh-cn/help/4093492/credssp-updates-for-cve-2018-0886-march-13-2018 参照官方更新文件&#xff1a;查找办法 https://support.microsoft.com/zh-cn/help/4093492/&#xff0c;4093492是更新包kb后面的数字 修改办法&#xff1a;下图参照注册表…

Redis分布式锁—Redisson+RLock可重入锁实现篇

前言 平时的工作中&#xff0c;由于生产环境中的项目是需要部署在多台服务器中的&#xff0c;所以经常会面临解决分布式场景下数据一致性的问题&#xff0c;那么就需要引入分布式锁来解决这一问题。 针对分布式锁的实现&#xff0c;目前比较常用的就如下几种方案&#xff1a;…

angular安装记录

1. 安装node.js&#xff0c;下载地址&#xff1a;https://nodejs.org/en/download/&#xff0c;详细的安装教程参考这里&#xff1a;https://blog.csdn.net/u010255310/article/details/52205132 直接一路next就可以。安装好node后&#xff0c;会自动在path中配置了node的安装路…

Docker (1) 基本概念和安装

Docker简介 什么是容器&#xff1f; 一种虚拟化的方案&#xff0c;操作系统级别的虚拟化。容器是一个轻量的、独立的、可执行的包&#xff0c;包含了执行它所需要的所有东西&#xff1a;代码、运行环境、系统工具、系统库、设置。很长一段时间中&#xff0c;容器是专门用于Linu…

vue获取浏览器地址栏参数(?及/)路由+非路由实现方式

1、? 参数 浏览器参数形式&#xff1a;http://javam4.com/m4detail?id1322914793170014208 1.1、路由取参方式 this.$route.query.id前端跳转方式&#xff1a; 一、onclick方式 <a title"测试数据"click"test(row.id)"target"_blank"&g…

python3.X 使用pip 离线安装whl包(转载)

转载https://blog.csdn.net/wangyaninglm/article/details/54177720 0. 绪论 断网的环境下配置python开发环境非常讨厌&#xff0c;本文旨在优雅暴力的解决这一问题。 生产环境 &#xff1a; windows 7 windows10 python 3.5.2 pip 1.5.2 友情提示&#xff1a;出现问题时候&…

PHP文件下载

步骤 //流的方式发送给浏览器 header("Content-Type: application/octet-stream"); //按照字节的返回给浏览器 header("Accept-Ranges: bytes"); //告诉浏览器文件的大小 header("Accept-Length: ".filesize(文件地址)); //以附件的形式发送给浏…

面试官:了解二叉树吗,平衡二叉树,红黑树?

前言 面试过程中&#xff0c;多多少少会问一点数据结构&#xff08;二叉树&#xff09;的问题&#xff0c;今天我们来复习一下二叉树的相关问题&#xff0c;文末总结。 1. 二叉树的由来 在 jdk1.8 之前&#xff0c;HashMap 的数据结构由「数组链表」组成&#xff0c;数组是 …

Jenkins: QQ/Wechat机器人群消息通知Job构建结果

简介 Jenkins是持续化集成的一个核心部件&#xff0c;它上游从仓库&#xff08;gitlab&#xff09;等拉取代码&#xff0c;经编译构建&#xff0c;将应用发布至下游目标环境&#xff1b;构建结果通知的方式有很多&#xff0c;现成的插件有邮件和钉钉方式&#xff0c;但是就方便…

Java中的Set对象去重

前言部分 Set<T> 去重相信大家一定不陌生&#xff0c;尤其是在 Set<String>、Set<Integer> 等等&#xff0c;但是在使用 Set<实体> &#xff0c;在不重写 equals()、hashCode() 方法情况下&#xff0c;直接使用貌似并不能生效。 所以想要 Set<实体…

openfalcon架构及相关服务配置详解

一&#xff1a;openfalcon组件 1.falcon-agent 数据采集组件 agent内置了一个http接口&#xff0c;会自动采集预先定义的各种采集项&#xff0c;每隔60秒&#xff0c;push到transfer。 2.transfer agent与transfer建立长连接&#xff0c;将数据汇报给tarnsfer transfer默认监听…

DBeaver连接达梦|虚谷|人大金仓等国产数据库

前言 工作中有些项目可能会接触到「达梦、虚谷、人大金仓」等国产数据库&#xff0c;但通常这些数据库自带的连接工具使用并不方便&#xff0c;所以这篇文章记录一下 DBeaver 连接国产数据库的通用模版&#xff0c;下文以达梦为例&#xff08;其他国产数据库连接操作方式一样&…

Map<String,Object>接收参数,Long类型降级为Integer,报类型转换异常

前言 今天看群里小伙伴问了一个非常有意思的问题&#xff1a; 使用 Map<String,Object> 对象接收前端传递的参数&#xff0c;在后端取参时&#xff0c;因为接口文档中明确该字段类型为 Long &#xff0c;所以对接收的参数进行了强转&#xff0c;即 (Long)参数 &#xf…