SimpleDateFormat线程不安全的原因以及解决方案

阿里巴巴java开发手册强制要求:

5. 【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。(org.apache.commons.lang3.time.DateUtils)

因为SimpleDateFormat 继承DateFormat,DateFormat中定义了属性calendar。SimpleDateFormat 里边parse(..)方法和format(..)方法都使用到了calendar,并且使用过程中,对于cal.clear(),cal.set*(..)都没有添加同步锁。所以SimpleDateFormat类的对象并不适合在多线程中共用。

如下所示:

SimpleDateFormat线程不安全的原因以及解决方案_第1张图片

SimpleDateFormat线程不安全的原因以及解决方案_第2张图片

SimpleDateFormat线程不安全的原因以及解决方案_第3张图片

SimpleDateFormat线程不安全的原因以及解决方案_第4张图片

SimpleDateFormat线程不安全的原因以及解决方案_第5张图片

多线程中使用SimpleDateFormat:

可以在每个线程中new一个自己的SimpleDateFormat对象。(对象无法复用,频繁的new和回收)

如果想在多线程中_共用SimpleDateFormat的对象,必须加锁。(synchronized导致降低多线程的效率)

可以使用import org.apache.commons.lang3.time.DateUtils进行一些时间的计算。

可以使用import org.apache.commons.lang3.time.DateFormatUtils进行日期和字符串的转换。

阿里巴巴java开发手册推荐使用ThreadLocal

private static final ThreadLocal df = new ThreadLocal() { 
    @Override 
    protected DateFormat initialValue() { 
        return new SimpleDateFormat("yyyy-MM-dd"); 
    } 
};

后边有代码示例。

注意:

6. 【强制】必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,

如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。

(如果使用代理模式和工厂模式使用线程池的话)尽量在代理中使用 try-finally 块进行回收。

正例:

objectThreadLocal.set(userInfo);

try {

// ...

} finally {

objectThreadLocal.remove();

}

如果通过new ThreadPoolExecutor(..)创建线程池的话,可以等线程执行完毕以后对ThreadLocal进行回收:threadLocal.remove();。或直接关闭线程池:threadPool.shutDown();

ThreadLocal代码示例:

    public static void main(String[] args) {
        ThreadLocal localDateFormat = new ThreadLocal();
        // 定义一个线程池
        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(4, 10, 10L, TimeUnit.SECONDS, 
                new LinkedBlockingQueue(20),
                Executors.defaultThreadFactory(), 
                new ThreadPoolExecutor.CallerRunsPolicy());
        // 线程计数器
        CountDownLatch main = new CountDownLatch(12);
        
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                String name = Thread.currentThread().getName();
                SimpleDateFormat dateFormat = localDateFormat.get();
                if (null == dateFormat) {
                    dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                    localDateFormat.set(dateFormat);
                    System.out.println(name+" 线程创建一个新的SimpleDateFormat");
                } else {
                    System.out.println(name+" 线程复用上次使用过的SimpleDateFormat");
                }
                // 线程任务执行完毕,主线程计数器减1。
                main.countDown();
            }
        };
        // 开辟线程执行任务
        for (int i = 0; i < 12; i++) {
            threadPool.execute(runnable);
            if (i == 3 || i == 7) {
                for (int j = 1; j <= 2100000000; j++) {
                    if (j == 2100000000) {
                        System.out.println("=================================");
                    }
                }
            }
        }

        try {
            // 线程阻塞,等到线程计数器归零后继续执行。
            main.await();
            System.out.println("所有线程任务执行完毕,回收localDateFormat。");
            localDateFormat.remove();
            System.out.println("线程池不用了。关闭线程池。如果之前没有对localDateFormat进行回收,线程池关闭后,线程里的localDateFormat会被回收。");
            threadPool.shutdown();
        } catch (InterruptedException e) {
            // shutdown()会在线程任务都执行完毕后将线程池关闭。
            threadPool.shutdown();
        }
    }

运行结果:

pool-1-thread-1 线程创建一个新的SimpleDateFormat
pool-1-thread-3 线程创建一个新的SimpleDateFormat
pool-1-thread-4 线程创建一个新的SimpleDateFormat
pool-1-thread-2 线程创建一个新的SimpleDateFormat
=================================
pool-1-thread-1 线程复用上次使用过的SimpleDateFormat
pool-1-thread-3 线程复用上次使用过的SimpleDateFormat
pool-1-thread-4 线程复用上次使用过的SimpleDateFormat
pool-1-thread-1 线程复用上次使用过的SimpleDateFormat
=================================
pool-1-thread-3 线程复用上次使用过的SimpleDateFormat
pool-1-thread-2 线程复用上次使用过的SimpleDateFormat
pool-1-thread-4 线程复用上次使用过的SimpleDateFormat
pool-1-thread-3 线程复用上次使用过的SimpleDateFormat
所有线程任务执行完毕,回收localDateFormat。
线程池不用了。关闭线程池。如果之前没有对localDateFormat进行回收,线程池关闭后,线程里的localDateFormat会被回收。

如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar, DateTimeFormatter 代替 SimpleDateFormat

推荐博文:DateTimeFormatter、LocalDateTime 的使用

你可能感兴趣的:(线程相关,ThreadLocal)