Java-SimpleDateFormat线程安全问题

文章目录

  • SimpleDateFormat
  • 使用
    • 示例
  • SimpleDateFormat问题
    • 线程不安全
      • 解决方案
        • 使用局部变量
        • 使用全局变量 & 操作加锁
        • 使用ThreadLocal进行线程隔离
        • JDK8-DateTimeFormatter
          • 用法

SimpleDateFormat

java.text.SimpleDateFormat是常用的时间日期格式化和解析的Java类。

SimpleDateFormat支持到毫秒精度

Java-SimpleDateFormat线程安全问题_第1张图片

使用

SimpleDateFormat最常用的方法是格式化日期时间和解析日期时间。

public class SimpleDateFormat extends DateFormat {
  
    //格式化日期时间
    public final String format(Date date) {......}
    
    //解析日期时间
    public Date parse(String source) throws ParseException {......}

    ......
}

示例

public static void main(String[] args) {
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss SSS");
    //格式化日期时间
    System.out.println(format.format(new Date()));
    System.out.println(format.format(new Date()));

    //精度只到毫秒
    System.out.println(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss SSS SSS").format(new Date()));

    try {
        //解析日期时间
        System.out.println(format.parse("2020-01-02 03:04:05 066"));
        System.out.println(format.parse("2020-01-02 03:04:05"));
    } catch (ParseException e) {
        //解析目标日期时间字符串格式不匹配
        e.printStackTrace();
    }
}

输出:

2021-06-01 08:24:08 404
2021-06-01 08:24:08 405
2021-06-01 08:24:08 405 405
Thu Jan 02 03:04:05 CST 2020
java.text.ParseException: Unparseable date: "2020-01-02 03:04:05"
    at java.text.DateFormat.parse(DateFormat.java:366)
    at com.wkw.study.jdk.SimpleDateFormatTest.main(SimpleDateFormatTest.java:26)

SimpleDateFormat问题

线程不安全

追踪日期格式化方法String format(Date date),发现

Java-SimpleDateFormat线程安全问题_第2张图片

这里使用了全局变量Calendar

/**
 * The {@link Calendar} instance used for calculating the date-time fields
 * and the instant of time. This field is used for both formatting and
 * parsing.
 *
 * 

Subclasses should initialize this field to a {@link Calendar} * appropriate for the {@link Locale} associated with this * DateFormat. * @serial */ protected Calendar calendar;

这个变量没有做线程安全控制,通常时间日期工具类中会有静态static方法,显然如果在静态时间日期工具类中使用了SimpleDateFormat,多线程并发调用时,就会争用calendar,比如 A 线程在执行设置的时候,刚好被 B 线程抢先设置了,日期时间格式化就会出现问题:

public static void main(String[] args) throws ParseException {
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String originDate1 = "2020-01-02 03:04:05";
    String originDate2 = "2021-11-22 13:14:15";
    Date date1 = format.parse(originDate1);
    Date date2 = format.parse(originDate2);
    new Thread(() -> {
        while (true) {
            String format1 = format.format(date1);
            if (!originDate1.equals(format1)) {
                System.out.println(originDate1 + " <> " + format1);
            }
        }
    }).start();
    new Thread(() -> {
        while (true) {
            String format2 = format.format(date2);
            if (!originDate2.equals(format2)) {
                System.out.println(originDate2 + " <> " + format2);
            }
        }
    }).start();
}

输出:

2020-01-02 03:04:05 <> 2021-11-22 13:14:15
2021-11-22 13:14:15 <> 2021-11-22 13:14:05
2020-01-02 03:04:05 <> 2020-01-22 13:14:15
2021-11-22 13:14:15 <> 2021-11-02 03:04:05
2020-01-02 03:04:05 <> 2020-01-22 13:14:15
2021-11-22 13:14:15 <> 2021-11-02 03:04:05
2021-11-22 13:14:15 <> 2020-01-02 03:04:05
2020-01-02 03:04:05 <> 2020-01-22 13:14:15
......

可以看到,就两个线程争用calendar,日期格式化结果就变的乱七八糟。

日期时间解析Date parse(String source)同样用到了calendar,一样的问题:

public static void main(String[] args) throws ParseException {
  SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  String originDate1 = "2020-01-02 03:04:05";
  String originDate2 = "2021-11-22 13:14:15";
  Date date1 = format.parse(originDate1);
  Date date2 = format.parse(originDate2);
  new Thread(() -> {
      while (true) {
          try {
              Date date = format.parse(originDate1);
              if (date1.compareTo(date) != 0) {
                  System.out.println(date1 + " <> " + date);
              }
          } catch (ParseException e) {
              e.printStackTrace();
          }
      }
  }).start();
  new Thread(() -> {
      while (true) {
          try {
              Date date = format.parse(originDate2);
              if (date2.compareTo(date) != 0) {
                  System.out.println(date2 + " <> " + date);
              }
          } catch (ParseException e) {
              e.printStackTrace();
          }
      }
  }).start();
}

输出:

Thu Jan 02 03:04:05 CST 2020 <> Thu May 14 10:04:05 CST 2020
Mon Nov 22 13:14:15 CST 2021 <> Sun May 13 03:30:48 CST 190728634
Mon Nov 22 13:14:15 CST 2021 <> Wed Sep 22 13:14:15 CST 2021
Exception in thread "Thread-0" java.lang.NumberFormatException: empty String
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
    at java.text.DigitList.getDouble(DigitList.java:169)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2089)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2162)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at com.wkw.study.jdk.SimpleDateFormatTest.lambda$main$0(SimpleDateFormatTest.java:24)
    at java.lang.Thread.run(Thread.java:748)

多线程下甚至都会抛异常。

在阿里巴巴 Java 开发手册中也规范的很清楚: Java-SimpleDateFormat线程安全问题_第3张图片

解决方案

使用局部变量
public static void main(String[] args) throws ParseException {
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String originDate1 = "2020-01-02 03:04:05";
    String originDate2 = "2021-11-22 13:14:15";
    Date date1 = format.parse(originDate1);
    Date date2 = format.parse(originDate2);
    new Thread(() -> {
      //线程内定义局部变量,不与其他线程争用SimpleDateFormat
      SimpleDateFormat localFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      while (true) {
          String format1 = localFormat.format(date1);
          if (!originDate1.equals(format1)) {
              System.out.println(originDate1 + " <> " + format1);
          }
      }
    }).start();
    new Thread(() -> {
      //线程内定义局部变量,不与其他线程争用SimpleDateFormat
      SimpleDateFormat localFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      while (true) {
          String format2 = localFormat.format(date2);
          if (!originDate2.equals(format2)) {
              System.out.println(originDate2 + " <> " + format2);
          }
      }
    }).start();
}

线程内定义局部变量,不与其他线程争用SimpleDateFormat

使用全局变量 & 操作加锁
public static void main(String[] args) throws ParseException {
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String originDate1 = "2020-01-02 03:04:05";
    String originDate2 = "2021-11-22 13:14:15";
    Date date1 = format.parse(originDate1);
    Date date2 = format.parse(originDate2);
    new Thread(() -> {
        while (true) {
            String format1 = SimpleDateFormatTest.dateFormat(format, date1);
            if (!originDate1.equals(format1)) {
                System.out.println(originDate1 + " <> " + format1);
            }
        }
    }).start();
    new Thread(() -> {
        while (true) {
            String format2 = SimpleDateFormatTest.dateFormat(format, date2);
            if (!originDate2.equals(format2)) {
                System.out.println(originDate2 + " <> " + format2);
            }
        }
    }).start();
}

//日期格式化加锁
public static synchronized String dateFormat(SimpleDateFormat format, Date date) {
    return format.format(date);
}
使用ThreadLocal进行线程隔离
public class SimpleDateFormatTest {

    private static ThreadLocal threadLocal = new ThreadLocal();

    public static void main(String[] args) throws ParseException {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String originDate1 = "2020-01-02 03:04:05";
        String originDate2 = "2021-11-22 13:14:15";
        Date date1 = format.parse(originDate1);
        Date date2 = format.parse(originDate2);
        new Thread(() -> {
            while (true) {
                String format1 = SimpleDateFormatTest.dateFormat(date1);
                if (!originDate1.equals(format1)) {
                    System.out.println(originDate1 + " <> " + format1);
                }
            }
        }).start();
        new Thread(() -> {
            while (true) {
                String format2 = SimpleDateFormatTest.dateFormat(date2);
                if (!originDate2.equals(format2)) {
                    System.out.println(originDate2 + " <> " + format2);
                }
            }
        }).start();
    }

    public static String dateFormat(Date date) {
        //使用ThreadLocal进行线程隔离
        SimpleDateFormat format = (SimpleDateFormat)threadLocal.get();
        if (format == null) {
            format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            threadLocal.set(format);
        }
        return format.format(date);
    }

}

JDK8-DateTimeFormatter
用法
public static void main(String[] args) throws ParseException {
    // String --> LocalTime
    LocalTime localTime = LocalTime.parse("07:43:53");
    System.out.println(localTime);

    // String --> LocalDate
    DateTimeFormatter formatter12 = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss"); // 12小时
    LocalDate localDate = LocalDate.parse("2019-12-07 07:43:53", formatter12);
    System.out.println(localDate);

    // String --> LocalDateTime
    DateTimeFormatter formatter24 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); // 24小时
    LocalDateTime localDateTime = LocalDateTime.parse("2019-12-07 07:43:53", formatter24);
    System.out.println(localDateTime);

    // LocalDateTime --> String
    String dateTime = localDateTime.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日"));
    System.out.println(dateTime);

    // LocalDateTime --> String
    System.out.println(localDateTime.format(DateTimeFormatter.ofPattern("yyyyMMdd HHmmss")));
}

输出

07:43:53
2019-12-07
2019-12-07T07:43:53
2019年12月07日
20191207 074353

LocalDateTime为例(LocalDateLocalTime同理)

**String 解析为 LocalDateTime**可以使用LocalDateTime.parse方法:

/**
 * Obtains an instance of {@code LocalDateTime} from a text string using a specific formatter.
 * 

* The text is parsed using the formatter, returning a date-time. * * @param text the text to parse, not null * @param formatter the formatter to use, not null * @return the parsed local date-time, not null * @throws DateTimeParseException if the text cannot be parsed */ public static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter) { ...... }

其中,DateTimeFormatter可以用DateTimeFormatter.ofPattern方法实例化:

/**
 * pattern为格式化 or 解析模式串
 */
public static DateTimeFormatter ofPattern(String pattern) { ...... }

LocalDateTime格式化为String可以使用format方法:

/**
 * Formats this date-time using the specified formatter.
 * 

* This date-time will be passed to the formatter to produce a string. * * @param formatter the formatter to use, not null * @return the formatted date-time string, not null * @throws DateTimeException if an error occurs during printing */ public String format(DateTimeFormatter formatter) { ...... }


参考:

SimpleDateFormat 为什么不是线程安全的?

SimpleDateFormat 性能问题

你可能感兴趣的:(java,源码,线程安全)