SimpleDateFormat 如何安全的使用?

前言

关注我

zhisheng

转载请务必注明原创地址为:http://www.54tianzhisheng.cn/2018/06/19/SimpleDateFormat/

在看的过程中有这么一条:

【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。

看到这条我立马就想起了我实习的时候有个项目里面就犯了这个错误,记得当时是这样写的:

所以才认真的去研究下这个 SimpleDateFormat,所以才有了这篇文章。

它是谁?

想必大家对 并不陌生。 是 Java 中一个非常常用的类,他是以区域敏感的方式格式化和解析日期的具体类。 它允许格式化 (date -> text)、语法分析 (text -> date)和标准化。

允许以任何用户指定的日期-时间格式方式启动。 但是,建议使用 中的 、 或 方法来创建一个日期-时间格式。 这几个方法会返回一个默认的日期/时间格式。 你可以根据需要用 方法修改格式方式。

日期时间格式

日期和时间格式由日期和时间模式字符串指定。在日期和时间模式字符串中,未加引号的字母 'A' 到 'Z' 和 'a' 到 'z' 被解释为模式字母,用来表示日期或时间字符串元素。文本可以使用单引号 (') 引起来,以免进行解释。所有其他字符均不解释,只是在格式化时将它们简单复制到输出字符串。

简单的讲:这些 A ——Z,a —— z 这些字母(不被单引号包围的)会被特殊处理替换为对应的日期时间,其他的字符串还是原样输出。

日期和时间模式(注意大小写,代表的含义是不同的)如下:

怎么使用?

日期/时间格式模版样例:(给的时间是:2001-07-04 12:08:56 U.S. Pacific Time time zone)

使用方法:

结果是:

使用方法很简单,就是先自己定义好时间/日期模版,然后调用 format 方法(传入一个时间 Date 参数)。

上面的是日期转换成自己想要的字符串格式。下面反过来,将字符串类型装换成日期类型:

结果是:

这个转换方法也很简单。但是不要高兴的太早,主角不在这。

线程不安全

在 SimpleDateFormat 类的 JavaDoc 中,描述了该类不能够保证线程安全,建议为每个线程创建单独的日期/时间格式实例,如果多个线程同时访问一个日期/时间格式,它必须在外部进行同步。那么在多线程环境下调用 format() 和 parse() 方法应该使用同步代码来避免问题。下面我们通过一个具体的场景来一步步的深入学习和理解SimpleDateFormat 类。

1、每个线程创建单独的日期/时间格式实例

大量的创建 SimpleDateFormat 实例对象,然后再丢弃这个对象,占用大量的内存和 JVM 空间。

2、创建一个静态的 SimpleDateFormat 实例,在使用时直接使用这个实例进行操作(我当时就是这么干的)

当然,这个方法的确很不错,在大部分的时间里面都会工作得很好,但一旦在生产环境中一定负载情况下时,这个问题就出来了。他会出现各种不同的情况,比如转化的时间不正确,比如报错,比如线程被挂死等等。我们看下面的测试用例,拿事实说话:

运行结果如下:

说明:Thread-1和Thread-0报java.lang.NumberFormatException: multiple points错误,直接挂死,没起来;Thread-2 虽然没有挂死,但输出的时间是有错误的,比如我们输入的时间是:2018-06-20 01:18:20 ,当会输出:Sat Jun 20 01:18:20 CST 2201 这样的灵异事件。

Why?

为什么会出现线程不安全的问题呢?

下面我们通过看 JDK 源码来看看为什么 SimpleDateFormat 和 DateFormat 类不是线程安全的真正原因:

SimpleDateFormat 继承了 DateFormat,在 DateFormat 中定义了一个 protected 属性的 Calendar 类的对象:calendar。只是因为 Calendar 类的概念复杂,牵扯到时区与本地化等等,JDK 的实现中使用了成员变量来传递参数,这就造成在多线程的时候会出现错误。

在 SimpleDateFormat 中的 format 方法源码中:

calendar.setTime(date) 这条语句改变了 calendar,稍后,calendar 还会用到(在 subFormat 方法里),而这就是引发问题的根源。想象一下,在一个多线程环境下,有两个线程持有了同一个 SimpleDateFormat 的实例,分别调用format 方法:

此时,calendar 已然不是它所设的值,而是走上了线程 2 设计的道路。如果多个线程同时争抢 calendar 对象,则会出现各种问题,时间不对,线程挂死等等。

分析一下 format 的实现,我们不难发现,用到成员变量 calendar,唯一的好处,就是在调用 subFormat 时,少了一个参数,却带来了许多的问题。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解。

这个问题背后隐藏着一个更为重要的问题--无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format 方法在运行过程中改动了 SimpleDateFormat 的 calendar 字段,所以,它是有状态的。

这也同时提醒我们在开发和设计系统的时候注意下一下三点:

1.自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明

2.多线程环境下,对每一个共享的可变变量都要注意其线程安全性

3.我们的类和方法在做设计的时候,要尽量设计成无状态的

解决方法

1、需要的时候创建新实例

说明:在需要用到 SimpleDateFormat 的地方新建一个实例,不管什么时候,将有线程安全问题的对象由共享变为局部私有都能避免多线程问题,不过也加重了创建对象的负担。在一般情况下,这样其实对性能影响比不是很明显的。

2、使用同步:同步 SimpleDateFormat 对象

说明:当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要 block 等待,多线程并发量大的时候会对性能有一定的影响。

3、使用 ThreadLocal

说明:使用 ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。

Java 8 中的解决办法

Java 8 提供了新的日期时间 API,其中包括用于日期时间格式化的 DateTimeFormatter,它与 SimpleDateFormat 最大的区别在于:DateTimeFormatter 是线程安全的,而 SimpleDateFormat 并不是线程安全。

DateTimeFormatter 如何使用:

解析日期

日期转换为字符串

由 DateTimeFormatter 的静态方法 ofPattern() 构建日期格式,LocalDateTime 和 LocalDate 等一些表示日期或时间的类使用 parse 和 format 方法把日期和字符串做转换。

使用新的 API,整个转换过程都不需要考虑线程安全的问题。

总结

SimpleDateFormat 是线程不安全的类,多线程环境下注意线程安全问题,如果是 Java 8 ,建议使用 DateTimeFormatter 代替 SimpleDateFormat。

参考资料

http://www.cnblogs.com/peida/archive/2013/05/31/3070790.htm

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180620G22M6D00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券