1.问题。
作为一个准毕业生,在平时的练习中鲜有遇见多线程的练习,所以从来没有意识到SimpleDateFormat还会有线程安全问题。前几天看了Leader分享的几篇博文后,对这个问题有了一个较为深刻的认识。
一般我们使用SimpleDateFormat的时候都会把它定义成一个静态变量,从而避免频繁地创建它的对象实例,那么问题来了。这样在多线程情况下它的实例就会被多个线程共享,从它的源码中看出它内部都是用了一个Calendar成员去完成日期的相关处理的,因此当多个线程去执行日期操作的相关代码时就会出现预料不到的情况,也就是说它是线程不安全的。
光说不练假把式,眼见为实,我们来运行一下代码开看看这种异常情况。
多次运行后会出现:
这里多次运行后同一个字符串日期在转换成Date类型后再转回字符串类型后同一个日期竟然不一致了,这是线程不安全的一种表现。那么如何才能避免这种问题呢?在Java7中SimpleDateFormat本身不是线程安全的,所以我们需要依赖其他一些手段来实现线程安全性,而在Java8中提供了线程安全的日期操作类。
2.在Java 7中的解决方案。
(1) 对象改为局部变量每一个线程拥有一个单独的SimpleDateFormat对象。 我们可以将SimpleDateFormat改为一个局部变量使每个线程拥有一个SimpleDateFormat对象实例,这样在多线程环境下每个线程都用属于自己的实例去操作日期,因为每个线程中程序是串行执行这样就可以保证线程安全性了。但是这种做法的缺点是每创建一个线程就会新创建一个SimpleDateFormat实例,当线程退出时该对象就会被销毁,这样就会频繁地创建和销毁对象,效率较低。
(2)使用ThreadLocal
首先我们用ThreadLocal创建一个日期工具类DateUtil,ThreadLocal以日期模式为key,以SimpleDateFormat为值,对于同一种日期模式,同一个线程,只会创建同一个SimpleDateFormat实例。
测试代码:
运行结果:
我们创建了6个任务,在一个线程中执行时同一个模式只需要创建一个对象实例,在2个线程中执行时也只需要创建四个对象实例,而要是用第一种方案就需要创建6个对象实例并且不能重用。这样既保证了线程安全性也保证了程序的性能。
(3)使用Joda-Time
Joda类具有不可变性,因此他们的实例无法被修改,具有不变性的类是线程安全的。处理日期计算的API方法全部返回一个对应Joda类的新实例,同时保持原实例不变。
多次运行,并未抛出日期转换异常。
3.在Java 8中的解决方案。
因为Java7中的java.util.Date和SimpleDateFormate有潜在的并发安全问题,这是我们开发人员不是很想处理的问题,为了解决这个问题,并在Java核心库中对时间和日期提供更好的支持,在Java8中提供了新的API。例如:LocalDate LocalTime LocalDateTime OffsetTime OffsetDateTime,我们可以利用它们来操作日期和时间。它们设计的核心思想就是把这些类设计为不可变类,就如同String对象一样。不可变对象显而易见是线程安全的。
package com.company; import java.time.LocalDate; /** * Created by zhu on 2017/4/5. */ public class DateFormatJava8 { static String[] testDate = {"2001-03-03", "1994-04-03", "2045-06-08"}; public static void main(String[] args) { Runnable[] runnables = new Runnable[testDate.length]; for (int i = 0; i < runnables.length; i++) { final int j = i; runnables[i] = new Runnable() { @Override public void run() { for (int k = 0; k < 1000; k++) { String originDate = testDate[j]; String formattedDate = null; LocalDate date = LocalDate.parse(testDate[j]); formattedDate = date.toString(); System.out.println("i: " + j + " k: " + k + " ThreadId: " + Thread.currentThread().getId() + " ThreadName: " + Thread.currentThread().getName() + " string2: " + formattedDate); if (!originDate.equals(formattedDate)) { throw new RuntimeException("date conversion failed after " + k + " iterations.Expected " + originDate + " but got " + formattedDate); } } } }; new Thread(runnables[i]).start(); } } }
多次运行,并未抛出日期转换异常。