前言
在日常开发中,我们经常需要去做日期格式转换,可能就会用到SimpleDateFormat
类。但是,如果使用不当,就很容易引发生产事故!
1. 问题推演
1.1 初始日期工具类
刚开始的日期转换工具类可能长这样:
public class DateUtil {
public static String formatDate(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
}
1.2 引入线程安全问题
这时候,就有人要说了,以上的代码存在问题,每次调用的使用,都要创建SimpleDateFormat
,在频繁使用时,就会创建大量的对象。
所以将代码改造成了这样:
public class DateUtil {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date) {
return sdf.format(date);
}
}
在这里,看似优化了性能,不管被调用多少次,都只有一个SimpleDateFormat对象,但是却引入了线程安全问题
1.3 并发问题示例
public class TestDateUtil {
public static void main(String[] args) throws InterruptedException {
// 创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
Date date1 = new Date(3600);
Date date2 = new Date(36000);
// 调用次数
int n = 10;
for (int i = 0; i < n; i++) {
int finalI = i;
executorService.execute(() -> {
if (finalI % 2 == 0) {
System.out.println("Date为:" + date1 + " 转换结果为:" + DateUtil.formatDate(date1));
} else {
System.out.println("Date为:" + date2 + " 转换结果为:" + DateUtil.formatDate(date2));
}
});
}
// 等待执行结果
executorService.shutdown();
}
}
输出结果:
Date为:Thu Jan 01 08:00:03 CST 1970 转换结果为:1970-01-01 08:00:03
Date为:Thu Jan 01 08:00:36 CST 1970 转换结果为:1970-01-01 08:00:36
Date为:Thu Jan 01 08:00:03 CST 1970 转换结果为:1970-01-01 08:00:03
Date为:Thu Jan 01 08:00:36 CST 1970 转换结果为:1970-01-01 08:00:03 // 错误结果
Date为:Thu Jan 01 08:00:36 CST 1970 转换结果为:1970-01-01 08:00:03 // 错误结果
Date为:Thu Jan 01 08:00:36 CST 1970 转换结果为:1970-01-01 08:00:03 // 错误结果
Date为:Thu Jan 01 08:00:36 CST 1970 转换结果为:1970-01-01 08:00:03 // 错误结果
Date为:Thu Jan 01 08:00:03 CST 1970 转换结果为:1970-01-01 08:00:36 // 错误结果
Date为:Thu Jan 01 08:00:03 CST 1970 转换结果为:1970-01-01 08:00:36 // 错误结果
Date为:Thu Jan 01 08:00:03 CST 1970 转换结果为:1970-01-01 08:00:03
可以看到上方出现了各种转换问题,【Thu Jan 01 08:00:36 CST 1970】的数据被转换成了【1970-01-01 08:00:03】。
1.4 阿里巴巴规范
阿里巴巴规范也提出,不要SimpleDateFormat
定义为static变量
2. 问题分析
查看源码,分析问题。
因为在SimpleDate类中,使用了成员变量在方法中进行传参调用,在多线程之间并发set、get中,很容易就产生了线程安全问题。
3. 解决方法
3.1 使用局部变量
使用局部变量,即最开始的用法,每一次都创建自己的SimpleDateFormat
对象,即可解决并发问题
public class DateUtil {
public static String formatDate(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
}
缺点:在高并发情况下会创建很多的对象,不推荐。
3.2 synchronized锁
使用synchronized
对存在线程安全的代码块进行同步处理
public class DateUtil {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date) {
synchronized (sdf) {
return sdf.format(date);
}
}
}
缺点:同一个时刻,只能有个一个线程执行format方法,性能比较差
3.3 ThreadLocal方式
使用ThreadLocal每个线程持有自己的SimpleDateFormat
,解决多线程之间并发问题
public class DateUtil {
// 创建 ThreadLocal 对象,并设置默认值(new SimpleDateFormat)
private static ThreadLocal<SimpleDateFormat> threadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static String formatDate(Date date) {
return threadLocal.get().format(date);
}
}
3.4 使用DateTimeFormatter
以上方案都是因为SimpleDateFormat
线程不安全导致我们需要去特殊处理,但在JDK 8
之后,可以直接使用线程安全类DateTimeFormatter
。
使用 DateTimeFormatter
必须要配合 JDK 8
中新增的时间对象 LocalDateTime
来使用,因此在操作之前,我们可以先将 Date
对象转换成 LocalDateTime
,然后再通过 DateTimeFormatter
来格式化时间,具体实现代码如下:
public class DateUtil {
// 创建 DateTimeFormatter 对象
private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date) {
// 将 Date 转换成 JDK 8 中的时间类型 LocalDateTime
LocalDateTime localDateTime =
LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
return dateTimeFormatter.format(localDateTime);
}
}
4. 各方案优缺点总结
如果是使用JDK 8+
,则直接使用DateTimeFormatter
即可。如果使用的是低版本的JDK,则可以使用TheadLocal
或synchronized
解决方案。