SimpleDateFormat是Java提供的一个格式化和解析日期的工具类,日常开发中应该经常会用到,但是由于它是线程不安全的,多线程公用一个SimpleDateFormat实例对日期进行解析或者格式化会导致程序出错。
代码示例演示
写一段小Demo来模拟多线程下SimpleDateFormat
做时间格式化的时候报错,代码如下:
1 | package com.vernon.test.demo.jdk.text; |
控制台正常的情况: 运气好~
1 | Connected to the target VM, address: '127.0.0.1:57434', transport: 'socket' |
控制台非正常的情况 运气不好~
1 | Connected to the target VM, address: '127.0.0.1:57756', transport: 'socket' |
通过IntelliJ IDEA的功能查看一下SimpleDateFormat的一个类关系图:
可知每个SimpleDateFormat
实例里面有一个Calendar
对象,从后面会知道其实SimpleDateFormat
之所以是线程不安全的就是因为Calendar
是线程不安全的,后者之所以是线程不安全的是因为其中存放日期数据的变量都是线程不安全的,比如里面的fields
、time
等。
1 | public Date parse(String text, ParsePosition pos) { |
1、解析日期字符串放入
CalendarBuilder
的实例calb
中;2、使用calb中解析好的日期数据设置
calendar
;3、重置日期对象
cal
的属性值;4、使用
calb
中中属性设置cal
;5、返回设置好的
cal
对象;
从上面步骤可知步骤3、4、5
操作不是原子性操作,当多个线程调用parse方法时候比如线程A执行了步骤3、4
也就是设置好了cal对象,在执行步骤5
前线程B执行了步骤3
清空了cal对象,由于多个线程使用的是一个cal对象,所以线程A执行步骤5
返回的就可能是被线程B清空后的对象,当然也有可能线程B执行了步骤4
被线程B修改后的cal对象,从而导致程序错误。
那么怎么解决呢?
第一种方式:
每次使用时候new一个SimpleDateFormat的实例,这样可以保证每个实例使用自己的Calendar实例,但是每次使用都需要new一个对象,并且使用后由于没有其它引用,就会需要被回收,开销会很大。
1 | package com.vernon.test.demo.jdk.text; |
第二种方式:
究其原因是因为多线程下步骤3、4、5三个步骤不是一个原子性操作,那么容易想到的是对其进行同步,让3、4、5成为原子操作,可以使用synchronized进行同步。
1 | package com.vernon.test.demo.jdk.text; |
第三种方式:
使用ThreadLocal,这样每个线程只需要使用一个SimpleDateFormat实例相比第一种方式大大节省了对象的创建销毁开销,并且不需要对多个线程直接进行同步,使用ThreadLocal方式。
1 | package com.vernon.test.demo.jdk.text; |
第四种方式:
在JDK8中新增了DateTimeFormatter
,由DateTimeFormatter
的静态方法ofPattern()
构建日期格式,LocalDateTime
和LocalDate
等一些表示日期或时间的类使用parse
和format
方法把日期和字符串做转换。使用新的API,整个转换过程都不需要考虑线程安全的问题。
1 | package com.vernon.test.demo.jdk.text; |
总结
Java8
发布,已有数年之久,但是发现很多人都还是坚持着用SimpleDateFormat
和Date
进行时间操作。SimpleDateFormat
这个类不是线程安全的,在使用的时候稍不注意,就会产生致命的问题。Date
这个类,是可以重新设置时间的,这对于一些类内部的属性来说,是非常不安全的。
如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员简栈文化-小助手(lastpass4u),他会拉你们进群。