【SimpleDateFormat】类线程不安全问题分析及解决方案

前言

在日常开发中,我们经常需要去做日期格式转换,可能就会用到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变量

image-20231003235317342

2. 问题分析

查看源码,分析问题。

image-20231003225715789

image-20231003230025194

因为在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,则可以使用TheadLocalsynchronized解决方案。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

丶只有影子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值