• 【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
    • 3
    • 4
    • 5
    • 6

    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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里,看似优化了性能,不管被调用多少次,都只有一个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();
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    输出结果:

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以看到上方出现了各种转换问题,【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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    缺点:在高并发情况下会创建很多的对象,不推荐。

    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);
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    缺点:同一个时刻,只能有个一个线程执行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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    4. 各方案优缺点总结

    如果是使用JDK 8+,则直接使用DateTimeFormatter即可。如果使用的是低版本的JDK,则可以使用TheadLocalsynchronized解决方案。

  • 相关阅读:
    C# 使用模式匹配的好处,因为好用所以推荐~
    Spring 随笔 ioc/di 2-依赖循环
    java毕业设计诊所信息管理系统Mybatis+系统+数据库+调试部署
    冲量在线入选北京市2022年第一批国家高新技术企业认定名单
    【Java基础】类型转换与常用运算符
    Java Reflection:Java反射简介说明
    区块链应用:椭圆曲线数字签名算法ECDSA
    静态类和非静态类的区别
    数据结构与算法
    RAD Studio 11.2详解其务实改进(Delphi & C++ Builder)-Alexandria
  • 原文地址:https://blog.csdn.net/weixin_43811294/article/details/133532067