聊聊 SimpleDateFormat 的线程安全问题

170 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 5 天,点击查看活动详情

1. 前言

SimpleDateFormat 类是 Java 中提供的日期时间的转化类,能够很方便地帮助我们处理时间格式化的问题。但是,在高并发的环境下,它会存在安全问题。在阿里巴巴 Java 开发手册中有强调:

SimpleDateFormat 不允许定义为 static 变量,若定义为 static,则必须加锁,或者使用DateUtils工具类。

2. 复现 SimpleDateFormat 线程安全问题

一般并发度不高的系统,遇到 SimpleDateFormat 出现线程安全问题的几率比较小,但是当并发量达到一定的量级时,线程安全问题就会出现。现在,我们使用线程池并结合 Java 并发包中的 CountDownLatch 类和 Semaphore 类来复现线程安全问题。

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

/**
 * @author zhongmingyi
 * @date 2023/2/7 1:30 下午
 */
public class SimpleDateFormatLearning {
    private static final int EXECUTE_COUNT = 1000; //执行总次数
    private static final int THREAD_COUNT = 20; //同时运行的线程数量
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");//SimpleDateFormat对象

    public static void main(String[] args) throws InterruptedException {
        final Semaphore semaphore = new Semaphore(THREAD_COUNT);
        final CountDownLatch countDownLatch = new CountDownLatch(EXECUTE_COUNT);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < EXECUTE_COUNT; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    try {
                        simpleDateFormat.parse("2023-02-07");
                    } catch (ParseException e) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    } catch (NumberFormatException e) {
                        System.out.println("线程:" + Thread.currentThread().getName() + " 格式化日期失败");
                        e.printStackTrace();
                        System.exit(1);
                    }
                    semaphore.release();
                } catch (InterruptedException e) {
                    System.out.println("信号量发生错误");
                    e.printStackTrace();
                    System.exit(1);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        System.out.println("所有线程格式化日期成功");
    }
}

执行结果如下:

image.png

由输出结果我们可以知道,simpleDateFormat 在高并发的情况下,其解析出现了异常,由此可以得出结论:SimpleDateFormat 类不是线程安全的。

3. 源码层面分析 SimpleDateFormat 类线程不安全

我们直接来看看 SimpleDateFormat 类的 parse 方法,其中关键的代码段如下:

image.png

通过调用 CalendarBuilder 的 establish 方法获得 Date 对象,我们来看看 establish 方法实现的关键:

image.png

它是先调用 clear() 方法,清除 Calendar 对象中设置的值,然后再调用 set 方法给对象设置值;但是 Calendar 对象并不是线程安全的(没有任何同步机制来保护),同时 clear 和 set 这两个操作并不是原子的,当多个线程同时操作一个 SimpleDateFormat 时就会引起混乱。

因此,可以输出结论:DateFormat 类中的 Calendar 对象被多线程共享,而 Calendar 对象本身不支持线程安全。

4. 解决 SimpleDateFormat 线程安全问题

(1)最简单:将 static 变量改为局部变量

将上述的代码中 SimpleDateFormat 这个静态变量:

private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

改造成每个线程里维护的局部变量:

executorService.execute(() -> {
   //... 省略代码
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");//SimpleDateFormat对象;
            simpleDateFormat.parse("2023-02-07");
   //... 省略代码
});

优点: 本方式的优点是实现简单

缺点: 缺点也很明显,每个线程都要创建一个对象,会占用和消耗过多的机器资源,不适用于高并发的场景

(2)加锁

由于代码中是将 SimpleDateFormat 类对象定义成全局静态变量,此时所有线程共享 SimpleDateFormat 类对象,此时在调用格式化时间的方法时,因此对SimpleDateFormat对象加锁同步即可。

同步的方式有多种,可以使用 synchronized 关键字;也可以使用 JUC 下的 ReentrantLock 的方式。

  • 使用 synchronized 关键字:

image.png

  • 使用 ReentrantLock 锁:

image.png

优点: 本方式的优点是无需创建局部变量,可以节省一定的资源开销

缺点: 同一时刻只能有一个线程抢到锁,对于高并发的场景会使系统的整体性能下降

(3)ThreadLocal

由于 ThreadLocal 具备每个线程都有一份对象的副本的特点,能够有效的避免多线程造成的线程安全问题。

image.png

优点: 此种方式运行效率比较高,推荐在高并发业务场景的生产环境使用

缺点: ThreadLocal 如果使用不当,会引发内存泄露

(4)将 SimpleDateFormat 替换成 DateTimeFormatter

DateTimeFormatter 是 Java8 提供的新的日期时间 API 中的类,DateTimeFormatter类是线程安全的,可以在高并发场景下直接使用 DateTimeFormatter 类来处理日期的格式化操作。代码比较简单就不贴出来了,感兴趣的同学可以自己学习下 DateTimeFormatter 的各类方法。