时间转化类SimpleDateFormat 的线程安全问题

1. 场景

在项目中经常用得到 SimpleDateFormat时间转化类,但是其并非线程安全的。可通过一个实例代码来说明。

import lombok.extern.slf4j.Slf4j;
@Slf4j
public class DateFormatExample1 {

    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
    }

    private static void update() {
        try {
            simpleDateFormat.parse("20180208");
        } catch (Exception e) {
            log.error("parse exception", e);
        }
    }
}

2. 分析使用到的类与方法是否线程安全。

  • 发布的对象是可变类且线程间共享
  • 多个线程可以同时修改该对象

分析sdf的源码可知SimpleDateFormat 类内部有一个 Calendar 对象引用,它用来储存和这个 SimpleDateFormat 相关的日期信息,例如sdf.parse(dateStr),sdf.format(date) 诸如此类的方法参数传入的日期相关String, Date等等。而这个发布的对象又可以被多个线程同时修改,进而导致了时间混乱的问题。

3. 线程安全策略

通过线程安全策略的一般方法来研究这个问题的解决办法,
---> 首先,无法将Calendar对象定义成不可变对象。
---> 其次,我们来看看线程封闭的策略,如果将sdf对象封闭在线程里,也就是每个线程创建一个 sdf对象,这样虽然避免了线程安全的问题,但是存在大量的资源消耗。这样我们再看 ThreadLocal的封闭策略,这种方式确实不错,我们通过控制最多的并发线程数可以限制实例化的sdf对象,并且封闭在各自的线程之中。
---> 同步容器,这里可以使用第三方 Joda-Time包

import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
@Slf4j
public class DateFormatExample3 {

    // 请求总数
    public static int clientTotal = 5000;

    // 同时并发执行的线程数
    public static int threadTotal = 200;

    private static DateTimeFormatter dateTimeFormatter = DateTimeFormat.forPattern("yyyyMMdd");

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();
        final Semaphore semaphore = new Semaphore(threadTotal);
        final CountDownLatch countDownLatch = new CountDownLatch(clientTotal);
        for (int i = 0; i < clientTotal; i++) {
            final int count = i;
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    update(count);
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
    }

    private static void update(int i) {
        log.info("{}, {}", i, DateTime.parse("20180208", dateTimeFormatter).toDate());
    }
}

4. ThreadLocal 使用要注意的问题

ThreadLocal的出现是一种空间换时间的思想的运用,是为了多线程环境下单线程内变量共享的问题。它的原理就是每个线程通过ThreadLocal.ThreadLocalMap,保存当前线程中所有ThreadLocal变量引用的key和值。相当于每个线程有各自的变量副本,线程内共享这个变量数据,线程间互不影响。

ThreadLocal有它自己的使用场景,比如Spring中用它了解决Session、Connection等多线程并发访问问题,但不能它不能用来代替为了解决多线程安全问题的同步关键字,因为它实际上没有多线程间的变量共享,而线程安全问题是指多线程间变量共享,且共享变量可修改,进而可能会出现多线程并发修改共享变量的问题,这种需要通过同步手段解决。

ThreadLocal变量一般要声名成static类型,即当前线程中只有一个T类型变量的实例,线程内可共享该实例数据且不会出问题,如将其声名成非static,则一个线程内就存储多个T类型变量的实例,有点存储空间的浪费,一般很少有这样的应用场景。另外根据实际情况,ThreadLocal变量声名时也多加上private final关键词表明它时类内私有、引用不可修改。

在线程池环境下,由于线程是一直运行且复用的,使用ThreadLocal时会出现这个任务看到上个任务ThreadLocal变量值以及内存泄露等问题,解决方法就是在当前任务执行完后将ThreadLocal变量remove。

5. DateTimeFormatter (Java 8支持)

更新下,Java 8内置的DateTimeFormatter是线程安全的,放心使用。
https://www.liaoxuefeng.com/wiki/1252599548343744/1303985694703650

参考链接:
https://blog.csdn.net/zq602316498/article/details/40263083
https://www.cnblogs.com/zhuimengdeyuanyuan/archive/2017/10/25/7728009.html

Java面试必问,ThreadLocal终极篇

你可能感兴趣的:(时间转化类SimpleDateFormat 的线程安全问题)