背景 在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给实际开发中带来的便利:
|