一:概述
SimpleDateFormat 类主要负责日期的转换与格式化等操作,在多线程的环境中,使用此类容易造成数据转换及处理的不正确,因为 SimpleDateFormat 类并不是线程安全的,但在单线程环境下是没有问题的。
SimpleDateFormat 在类注释中也提醒大家不适用于多线程场景:
说的很清楚,SimpleDateFormat 不是线程安全的,多线程下需要为每个线程创建不同的实例。
不安全的原因是因为使用了 Calendar 这个全局变量:
在日期格式化的时候:
二:线程不安全的原因
我们把重点放在 calendar ,这个 format 方法在执行过程中,会操作成员变量 calendar 来保存时间 calendar.setTime(date) 。
但由于在声明 SimpleDateFormat 的时候,使用的是 static 定义的,那么这个 SimpleDateFormat 就是一个共享变量,SimpleDateFormat 中的 calendar 也就可以被多个线程访问到,所以问题就出现了,举个例子:
假设线程 A 刚执行完 calendar.setTime(date) 语句,把时间设置为 2020-09-01,但线程还没执行完,线程 B 又执行了 calendar.setTime(date) 语句,把时间设置为 2020-09-02,这个时候就出现幻读了,线程 A 继续执行下去的时候,拿到的 calendar.getTime 得到的时间就是线程B改过之后的。
除了 format() 方法以外,SimpleDateFormat 的 parse 方法也有同样的问题。
至此,我们发现了 SimpleDateFormat 的弊端,所以为了解决这个问题就是不要把 SimpleDateFormat 当做一个共享变量来使用
阿里巴巴 java 开发规范是怎么描述 SimpleDateFormat 的:
三、模拟线程安全问题
无码无真相,接下来我们创建一个线程来模拟 SimpleDateFormat 线程安全问题:
创建 TestThread.java 类:
结果:
从控制台打印的结果来看,使用单例的 SimpleDateFormat 类在多线程的环境中处理日期转换,极易出现转换异常(java.lang.NumberFormatException:multiple points)以及转换错误的情况。
代码:
public class TestThread extends Thread {private SimpleDateFormat simpleDateFormat;// 要转换的日期字符串private String dateString;public TestThread(SimpleDateFormat simpleDateFormat, String dateString) {this.simpleDateFormat = simpleDateFormat;this.dateString = dateString;}@Overridepublic void run() {try {Date date = simpleDateFormat.parse(dateString);String newDate = simpleDateFormat.format(date).toString();if (!newDate.equals(dateString)) {System.out.println("ThreadName=" + this.getName()+ " 报错了,日期字符串:" + dateString+ " 转换成的日期为:" + newDate);}} catch (ParseException e) {e.printStackTrace();}}
}// 一般我们使用SimpleDateFormat的时候会把它定义为一个静态变量,避免频繁创建它的对象实例private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY-MM-dd");public static void main(String[] args) {String[] dateStringArray = new String[]{"2021-03-10", "2021-05-1", "2021-05-19", "2021-09-17"};TestThread[] threads = new TestThread[4];// 创建线程for (int i = 0; i < 4; i++) {threads[i] = new TestThread(simpleDateFormat, dateStringArray[i]);}// 启动线程for (int i = 0; i < 4; i++) {threads[i].start();}}
五、如何解决线程安全
1、每次使用就创建一个新的 SimpleDateFormat
创建全局工具类 DateUtils.java
public class DateUtils {public static Date parse(String formatPattern, String dateString) throws ParseException {return new SimpleDateFormat(formatPattern).parse(dateString);}public static String format(String formatPattern, Date date){return new SimpleDateFormat(formatPattern).format(date);}
}
2:加synchronized 锁
简单粗暴,synchronized 往上一套也可以解决线程安全问题,缺点自然就是并发量大的时候会对性能有影响,因为使用了 synchronized 加锁后的多线程就相当于串行,线程阻塞,执行效率低
public class DateUtilsSynchronized {private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static Date parse(String formatPattern, String dateString) throws ParseException {synchronized (simpleDateFormat){return simpleDateFormat.parse(dateString);}}public static String format(String formatPattern, Date date) {synchronized (simpleDateFormat){return simpleDateFormat.format(date);}}
}
3、使用ThreadLocal
ThreadLocal 是 java 里一种特殊的变量,ThreadLocal 提供了线程本地的实例,它与普通变量的区别在于,每个使用该线程变量的线程都会初始化一个完全独立的实例副本。
hreadLocal 可以确保每个线程都可以得到单独的一个 SimpleDateFormat 的对象,那么就不会存在竞争问题。
public class DateUtilsThreadLocal {private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {@Overrideprotected DateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd");}};public static Date parse(String formatPattern, String dateString) throws ParseException {return threadLocal.get().parse(dateString);}public static String format(String formatPattern, Date date) {return threadLocal.get().format(date);}
}
4:推荐写法
上边提到的阿里巴巴 java 开发手册给出了说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替 SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。
日期转换,SimpleDateFormat 固然好用,但是现在我们已经有了更好地选择,Java 8 引入了新的日期时间 API,并引入了线程安全的日期类,一起来看看。
Instant:瞬时实例。
LocalDate:本地日期,不包含具体时间 例如:2014-01-14 可以用来记录生日、纪念日、加盟日等。
LocalTime:本地时间,不包含日期。
LocalDateTime:组合了日期和时间,但不包含时差和时区信息。
ZonedDateTime:最完整的日期时间,包含时区和相对UTC或格林威治的时差。
新API还引入了 ZoneOffSet 和 ZoneId 类,使得解决时区问题更为简便。
解析、格式化时间的 DateTimeFormatter 类也进行了全部重新设计。
例如,我们使用 LocalDate 代替 Date,使用 DateTimeFormatter 代替 SimpleDateFormat,如下所示:
String DateNow = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"));System.out.println(DateNow);
这样就避免了 SimpleDateFormat 的线程不安全问题啦。