SimpleDateFormat在多线程下不安全?

2022-08-01,,

最近公司统一了下开发规范,其中有一条就是使用SimpleDateFormat时候不要设置成类的静态成员变量,被各个方法引。而是,建议改成方法内部变量,或者借助下ThreadLocal。

作为菜鸟的我,当时听到时,内心绝对的是无数个问号,当然了,听会的时候还是要强行装淡定嘛,假装我懂了。emm,,大家不要学我哦,所谓,子曰:知之为知之,不知为不知,是知也!

To 孔子老师:我错了,所以我来认真学习了,写下这笔记可好?

问题复现

废话别说了,赶紧重现下,别想糊弄到屏幕面前的各位大佬 !!!

下面的代码主要是开启10个线程,每个线程都去调用下格式化日期的方法,会发现结果10次调用的结果中,会出现重复日期,呃呃呃。

public class DateUtil {
 private static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");

    //工具类:私有构造
    private DateUtil() {
    }

    public static String format(Integer second) {
        return DATE_FORMAT.format(second * 1000);
    }

    //测试下
    public static void main(String[] args) {
        //启动10个固定大小的线程池,调用10次的格式化方法
        ThreadFactory factory = new ThreadFactoryBuilder().setDaemon(true).setNameFormat("DateUtil-%s").build();
        ExecutorService executorService = Executors.newFixedThreadPool(10, factory);
        //计数器:当值被减到0的时候,await方法将不会被阻塞
        CountDownLatch countDownLatch = new CountDownLatch(10);
        //用来存放10次调用格式化后的字符串结果(利用set的不可重复性)
        Set<String> dateStrSet = Sets.newConcurrentHashSet();
        for (int i = 1; i <= 10; i++) {
            final int second = i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        String dateStr = DateUtil.format(second);
                        dateStrSet.add(dateStr);
                        System.out.println(Thread.currentThread().getName() + ":======>" + dateStr);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    //每执行完一次调用,计数器减1
                    countDownLatch.countDown();
                }
            });
        }

        //计数器未完成,则线程将阻塞在这里
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(" set集合的大小:======>" + dateStrSet.size());
    }
}
}

运行结果:

DateUtil-2======>1970-01-01 08:00:03 000
DateUtil-3======>1970-01-01 08:00:04 000
DateUtil-1======>1970-01-01 08:00:03 000
DateUtil-0======>1970-01-01 08:00:03 000
DateUtil-5======>1970-01-01 08:00:07 000
DateUtil-6======>1970-01-01 08:00:07 000
DateUtil-4======>1970-01-01 08:00:07 000
DateUtil-7======>1970-01-01 08:00:08 000
DateUtil-8======>1970-01-01 08:00:09 000
DateUtil-9======>1970-01-01 08:00:10 000
 set集合的大小:======>6

天呐,我这里只是10个线程啊,这重复率有点高吧~

理想的结果应该是10个不重复的日期字符串的。

分析原因

主要原因:
格式化日期,是借助将Date里存储的时间戳(毫秒级)set到
java.util.Calendar 中,而这个Calendar在SimpleDateFormat 中是一个类的成员变量。多线程调用同一个SimpleDateFormat对象的format方法的时候,使用的是同一个Calendar对象 。 讲到这里,应该都懂了吧 。 并发情况下,如果操作同一个SimpleDateForamt的Calendar变量,会引发线程不安全的问题。

源码段1 : format 方法

SimpleDateFormat.java#format方法

Calendar 是SimpleDateFormat 父类DateFormat里的一个成员变量

源码段2 : calendar在哪里被设值的?

SimpleDateFormat.java#构造方法

比如:线程A 把Calendar 改成了 2020-07-20 18:51:00 ,这时候,并发来个线程B ,又把Calendar 改成 2020-07-20 18:51:01 ,,最终线程A调用format返回的值将会是线程B改了之后的值 。这样就会出现,线程A 和线程B 调用format 方法返回值是一样的呢。

解决办法

思路: 每个线程能有着独立的Calendar

(1)内部类成员变量改成内部方法变量

意思就是,在你的工具类里面,每次format的时候,就new SimpleDateFormat ,避免多个线程之间共用SimpleDateFormat对象的问题啦。

public class DateUtil {

    //工具类:私有构造
    private DateUtil() {
    }

    public static String format(Integer second) {
        SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
        return DATE_FORMAT.format(second * 1000);
    }

    public static void main(String[] args) {
        ThreadFactory factory = new ThreadFactoryBuilder().setDaemon(true).setNameFormat("DateUtil-%s").build();
        ExecutorService executorService = Executors.newFixedThreadPool(10, factory);
        Set<String> dateStrSet = Sets.newConcurrentHashSet();
        CountDownLatch countDownLatch = new CountDownLatch(1000);
        for (int i = 1; i <= 1000; i++) {
            final int second = i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        String dateStr = DateUtil.format(second);
                        dateStrSet.add(dateStr);
                        System.out.println(Thread.currentThread().getName() + ":======>" + dateStr);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    countDownLatch.countDown();
                }
            });
        }

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(" set集合的大小:======>" + dateStrSet.size());
    }
 }

运行结果:

。。。这里省略了很多日期的打印,可直接看最后的set集合大小
DateUtil-8======>1970-01-01 08:16:38 000
DateUtil-8======>1970-01-01 08:16:39 000
DateUtil-8======>1970-01-01 08:16:40 000
DateUtil-3======>1970-01-01 08:15:36 000
DateUtil-4======>1970-01-01 08:15:31 000
DateUtil-9======>1970-01-01 08:15:30 000
DateUtil-7======>1970-01-01 08:15:29 000
DateUtil-5======>1970-01-01 08:15:27 000
DateUtil-0======>1970-01-01 08:15:26 000
DateUtil-2======>1970-01-01 08:15:25 000
DateUtil-6======>1970-01-01 08:15:23 000
DateUtil-1======>1970-01-01 08:15:38 000
 set集合的大小:======>1000

这里看出,起码循环调用了format方法1000次,最终产生的格式后的字符产放在了Set集合中,Set的最终大小也是1000个,说明这种方法是可以解决SimpleDateFormat的线程不安全问题的。

(2)ThreadLocal 线程隔离

上面的方法,是能够解决问题的。但是可能追求高质量代码的大佬们,看着肯定就不舒服,就知道天天new ,非要给垃圾回收增加负担是吧!!!

行吧,再支一招呗。使用ThreadLocal< SimpleDateFormat > 让每个线程有着独立的SimpleDateFormat , 这样不就等同于每个线程有着独立的Calendar 啦 ~

import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ThreadFactoryBuilder;

import java.text.SimpleDateFormat;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

public class DateUtil {

    private static String DATE_FORMAT_DEFAULT = "yyyy-MM-dd HH:mm:ss SSS";
    private static ThreadLocal<SimpleDateFormat> DATE_FORMAT_LOCAL = new ThreadLocal<SimpleDateFormat>() {
        //给每个线程创建一个SimpleDateFormat对象并存储到ThreadLocalMap中
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat(DATE_FORMAT_DEFAULT);
        }
    };

    //工具类:私有构造
    private DateUtil() {
    }

    public static String format(Integer second) {
        return DATE_FORMAT_LOCAL.get().format(second * 1000);
    }

    //测试下
    public static void main(String[] args) {
        ThreadFactory factory = new ThreadFactoryBuilder().setDaemon(true).setNameFormat("DateUtil-%s").build();
        ExecutorService executorService = Executors.newFixedThreadPool(10, factory);
        CountDownLatch countDownLatch = new CountDownLatch(1000);
        Set<String> dateStrSet = Sets.newConcurrentHashSet();
        for (int i = 1; i <= 1000; i++) {
            final int second = i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    try {
                        String dateStr = DateUtil.format(second);
                        dateStrSet.add(dateStr);
                        System.out.println(Thread.currentThread().getName() + ":======>" + dateStr);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    //每执行完一次调用,计数器减1
                    countDownLatch.countDown();
                }
            });
        }

        //计数器为完成,则线程将阻塞在这里
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(" set集合的大小:======>" + dateStrSet.size());
    }
}

运行结果:

。。。这里省略了很多日期的打印,可直接看最后的set集合大小
DateUtil-4======>1970-01-01 08:16:40 000
DateUtil-6======>1970-01-01 08:16:15 000
DateUtil-2======>1970-01-01 08:16:14 000
DateUtil-9======>1970-01-01 08:16:13 000
DateUtil-0======>1970-01-01 08:13:27 000
DateUtil-8======>1970-01-01 08:13:49 000
DateUtil-1======>1970-01-01 08:13:29 000
DateUtil-3======>1970-01-01 08:13:18 000
DateUtil-7======>1970-01-01 08:13:14 000
DateUtil-5======>1970-01-01 08:16:27 000
 set集合的大小:======>1000

这里也能看出来,使用ThreadLocal 也可以解决SimpleDateFormat的线程不安全的问题。

我现在能猜出,屏幕之前的大佬们肯定又要考我了。

“你不知道ThreadLocal有内存泄漏的问题么?确定这样可以?”

好了,到这里又引发了另一个热点问题:ThreadLocal你了解多少 !

这里就要来点弱引用的定义:

弱引用:这里讨论ThreadLocalMap中的Entry类的重点,如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器回收掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象被回收掉之后,再调用get方法就会返回null。

再来段代码:


import java.lang.ref.WeakReference;
import java.util.concurrent.TimeUnit;

public class TestWeakReference {
    public static void main(String[] args) {
        WeakReference<Long> weakReference = new WeakReference<Long>(new Long(1));
        System.out.println("触发gc之前:" + weakReference.get());
        System.gc();
        while (weakReference.get() != null) {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("触发gc之后:" + weakReference.get());
        }
    }
}

运行结果:

从这结果可以看出,weakReference 构造的时候,是通过new Long()的方式和该Long类型的对象建立了引用,但这个Long类型的对象除了这个弱引用使用了,其它都没有应用,手动触发垃圾回收,当然就把这个孤零零的Long对象给清掉了呀~

改下代码:

        Long longKey = new Long(1);
        WeakReference<Long> weakReference = new WeakReference<Long>(longKey);

运行效果:

无限的【触发gc之后:1】输出到了控制台,我等了好久,我希望它停下来啊,估计是永远等不了。

为啥?因为 Long longKey 是个强引用,并且在main函数未结束的时候,绝对不会回收这个强引用的。只有当longKey被回收了,那么longKey只剩下弱引用在使用,这时候,弱引用也会被回收!

记住什么情况下才会回收弱引用

  • 如果一个对象只具有弱引用
  • 垃圾回收启动了

再回归到上面考察题的正确答案:上述的 DATE_FORMAT_LOCAL被定义的是个static变量呀,正常来说,ThreadLocalMap引用的这个变量是个static类型的呀,啥时候会被回收?JVM在垃圾回收的时候应该不会回收吧!所以别担心了。

本文地址:https://blog.csdn.net/u014240299/article/details/107468618

《SimpleDateFormat在多线程下不安全?.doc》

下载本文的Word格式文档,以方便收藏与打印。