项目中常常会用到日期格式化,一般是使用SimpleDateFormat,某天偶然听某同事谈到偶尔会有日期转换报错,于是研究了一下。
查看SimpleDateFormat源码,发现作者有一段注释如下:
原来,SimpleDateFormat并不是线程安全的,作者推荐为每一个线程创建一个单独的实例,或者为SimpleDateFormat加锁。
再看同事的代码,SimpleDateFormat是一个静态变量,没有加锁,且直接提供给N多方法调用,看来原因就在这里。
为什么SimpleDateFormat不是线程安全的呢?
通过源码可以知道SimpleDateFormat的format方法实际操作的就是Calendar。
因为我们声明SimpleDateFormat为static变量,那么它的Calendar变量也就是一个共享变量,可以被多个线程访问。
假设线程A执行完calendar.setTime(date),把时间设置成2019-01-02,这时候被挂起,线程B获得CPU执行权。线程B也执行到了calendar.setTime(date),把时间设置为2019-01-03。线程挂起,线程A继续走,calendar还会被继续使用(subFormat方法),而这时calendar用的是线程B设置的值了,而这就是引发问题的根源,出现时间不对,线程挂死等等。
原来如此,那么如何解决呢?
解决方案
一、每次使用新创建对象
哪里用到哪里建,就没有啥线程安全问题了,但是增加GC负担,不推荐。
二、加锁
private static SimpleDateFormat formater = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date) {
synchronized (formater) {
return formater.format(date);
}
}
public static Date parseDate(String dateStr) throws ParseException {
synchronized (formater) {
return formater.parse(dateStr);
}
}
简单暴力,缺点是高并发下略微有点影响性能,阻塞线程。
三、ThreadLocal
private static ThreadLocal<DateFormat> threadFormater = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static String tlFormatDate(Date date) {
return threadFormater.get().format(date);
}
public static Date tlParseDate(String dateStr) throws ParseException {
return threadFormater.get().parse(dateStr);
}
综合一二的优缺点,解决了线程竞争问题,安全高效。
四、基于JDK1.8的DateTimeFormatter
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static String jdk8FormatDate(LocalDateTime date) {
return dateTimeFormatter.format(date);
}
public static LocalDateTime jdk8ParseDate(String dateStr) throws ParseException {
return LocalDateTime.parse(dateStr, dateTimeFormatter);
}
java.time.format.DateTimeFormatter是jdk新加入的日期格式化工具类,解决了SimpleDateFormat的线程安全问题,如果使用的是jdk8以上,强烈推荐使用。缺点嘛,要使用instant代替Date,LocalDateTime替代Calendar,对老代码来说有点难受。