【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变量

【SimpleDateFormat】线程不安全问题分析及解决方案_第1张图片

2. 问题分析

查看源码,分析问题。

【SimpleDateFormat】线程不安全问题分析及解决方案_第2张图片

【SimpleDateFormat】线程不安全问题分析及解决方案_第3张图片

因为在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解决方案。

你可能感兴趣的:(Java工具方法,java,开发语言)