SimpleDateFormat线程不安全问题与解决思路

问题复现

public class SimpleDateFormatDemo {

    public static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(20);
        for (int i = 0; i < 200; i++) {
            pool.execute(() -> {
                try {
                    System.out.println(sdf.parse("2022-04-15 17:02:00"));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

执行结果

Fri Apr 15 17:02:00 CST 2022
Fri Apr 15 17:02:00 CST 2022
Fri Apr 15 17:02:00 CST 2022
Wed Apr 15 17:02:00 CST 44
Tue Apr 15 17:02:00 CST 2200
Wed Apr 15 17:02:00 CST 44
Sun Apr 17 05:42:00 CST 2022
Fri Apr 15 17:02:00 CST 2022
Mon Apr 15 17:02:00 CST 1720
Mon Apr 18 00:35:20 CST 2022
Thu Apr 15 17:02:00 CST 1
Wed Mar 31 17:02:00 CST 1
Fri Apr 15 17:02:00 CST 2022
Thu Apr 15 17:02:00 CST 4202
Fri Apr 15 17:02:00 CST 2022
Fri Apr 15 21:40:00 CST 2022

原因分析

SimpleDateFormat继承于DateFormat类,其中有一个Calendar对象,用于format和parse时计算时间。每次执行parse()时,会先将Calendar对象清空后重新计算得到新的值,这个过程是没有加锁的,此时如果其他线程同时在执行,则会导致获取不到Calendar对象正确的值,最终parse的结果出错。


SimpleDateFormat 的 JavaDoc 也明确指出:

Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.

解决思路

1. 与ThreadLocal结合使用(推荐)

public class SimpleDateFormatDemo {
    
    private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(20);
        for (int i = 0; i < 200; i++) {
            pool.execute(() -> {
                try {
                    System.out.println(formatter.get().parse("2022-04-15 17:02:00"));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

2. 使用apache commons-lang包的FastDateFormat类

FastDateFormat is a fast and thread-safe version of java.text.SimpleDateFormat.

format和parse的使用方式如下:

FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss").format(new Date());
FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss").parse("2022-04-15 17:02:00")

3. 使用Java8提供的DateTimeFormatter类

format和parse的使用方式如下:

LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime.parse("2022-04-15 17:02:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));

4. 其他思路:

  • 使用Synchronized或ReentrantLock加锁

  • 每次转换时new一个SimpleDateFormat实例

这两种方法都不推荐:前者在并发很高时会导致显著的性能下降,而后者会创建大量一次性对象,加大GC的压力。

参考文章

  • A Guide to SimpleDateFormat
  • SimpleDateFormat 的线程安全问题与 ThreadLocal

你可能感兴趣的:(SimpleDateFormat线程不安全问题与解决思路)