生活中,我们需要掌控自己的时间,减少加班,提高效率;日常开发中,我们需要操作时间API,保证效率、安全、稳定。现在都2020年了,了解如何在JDK8及以后的版本中更好地操控时间就很有必要,尤其是一次线上BUG的发生,让小明更是深有体会。
背景
在Java8以前,每每操控时间,我们经常使用的类库就是Date,并且会通过SimpleDateFormat类对时间进行格式化。你可知道?Date类是一个可变类,SimpleDateFormat类也是线程不安全的,因此在多线程的场景下执行格式化操作时,就会发生意想不到的情况。下面我们看一下使用Date、SimpleDateFormat在多线程下可能发生的问题以及使用LocalDateTime、DateTimeFormatter的方法和优势。
问题来了
多线程环境下,使用Date、SimpleDateFormat时,如果我们将它定义为一个静态变量使用,虽然会避免重复创建实例, 但是会出现个别线程获取时间失败的现象,我们通过代码模拟这个场景:
运行main方法,查看控制台会发现有个别线程会报java.lang.NumberFormatException异常。类似下图所示:
问题分析
接下来,我们通过查看源码进一步分析(多图预警),可以看到SimpleDateFormat是直接继承的DateFormat类:
并重写了**parse()**(字符串转日期)和 **format()**(日期转字符串)方法,因此我们重点从这两个方法来分析。
首先是SimpleDateFormat的parse()**方法,该方法中创建了一个CalendarBuilder**对象,
再往下看,会看到CalendarBuilder使用establish方法将变量calendar设值到其属性中,
![image-20200420012213545](/Users/xin/Library/Application Support/typora-user-images/image-20200420012213545.png)
而calendar是父类DateFormat类的共享变量,可以被多个线程访问到
因此当SimpleDateFormat声明为static时,线程并不安全,多个线程同时操作访问就会抛出异常。
同样地通过查看format()**,我们发现format方法中有一行calendar.setTime(date)
;也是操作的该共享变量calendar**,线程也是不安全的。
有趣的是,在DateFormat源码注释上作者也已经给出醒目的提示:
使用Google翻译过来就是
日期格式不同步。 建议为每个线程创建单独的格式实例。 如果多个线程同时访问一种格式,则必须在外部同步该格式。
解决方案
小明有一句座右铭,方法总比问题多。我们来看几个小明认为不错的解决方案。
1、仅在需要用到的地方创建一个新的实例,就没有线程安全问题。
点评:加重了创建对象的负担,频繁地创建和销毁对象,消耗资源,效率较低。
2、通过synchronized解决线程安全问题;
点评:并发量大的时候会对性能有影响,容易造成线程阻塞。
3、通过ThreadLocal保证线程之间变量不共享
点评:ThreadLocal可以确保每个线程都可以得到单独的一个SimpleDateFormat的对象,那么自然也就不存在竞争问题了。就是有点大材小用。
以上就是小明能够提供的所有方案。什么,都不满意?我们来看一下2020年JDK8的解决方案。
使用LocalDateTime
在Java8以后,我们有了新的选择,使用LocalDateTime时间类。首先,LocalDateTime本身是线程安全的,其对应的格式化工具类DateTimeFormatter也是线程安全的,不存在变量共享,每一个属性字段都用了final关键字修饰,因此每次操作后都是返回的copy对象。并且LocalDateTime类本身也有很多操作时间的API来替代传统的Calendar类。
基于Java8的DateTimeFormatter的解决方案,我们对之前的代码进行改造,多线程环境下,运行代码,并未发现任何异常,稳定高效:
我们可以看到在DateTimeFormatter源码上作者也贴心的加注释说明,该类是不可变的,并且是线程安全的。
同理,这点我们也可以从LocalDateTime的官方源码中看出。
其他骚操作
为了让大家忘掉之前使用Calendar操作时间的笨拙,我们来切实感受一下LocalDateTime给实际开发中带来的便利:
更多举例说明,请点击文末阅读原文
总结
综上,小明推荐小伙伴们使用JDK8的LocalDateTime系列来取代Date系列,这样做不仅能够保证线上项目平稳运行,而且通过其自带的API还能操作时间,还能提高开发效率,今晚可以不加班!