欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

SimpleDateFormat在多线程下不安全?

程序员文章站 2022-10-03 18:50:40
最近公司统一了下开发规范,其中有一条就是使用SimpleDateFormat时候不要设置成类的静态成员变量,被各个方法引。而是,建议改成方法内部变量,或者借助下ThreadLocal。作为菜鸟的我,当时听到时,内心绝对的是无数个问号,当然了,听会的时候还是要强行装淡定嘛,假装我懂了。emm,,大家不要学我哦,所谓,子曰:知之为知之,不知为不知,是知也!To 孔子老师:我错了,所以我来认真学习了,写下这笔记可好?问题复现废话别说了,赶紧重现下,别想糊弄到屏幕面前的各位大佬 !!!分析原因解决办....

最近公司统一了下开发规范,其中有一条就是使用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方法

SimpleDateFormat在多线程下不安全?
Calendar 是SimpleDateFormat 父类DateFormat里的一个成员变量
SimpleDateFormat在多线程下不安全?

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

SimpleDateFormat.java#构造方法

SimpleDateFormat在多线程下不安全?
SimpleDateFormat在多线程下不安全?

比如:线程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());
        }
    }
}

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

改下代码:

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

运行效果:
SimpleDateFormat在多线程下不安全?
无限的【触发gc之后:1】输出到了控制台,我等了好久,我希望它停下来啊,估计是永远等不了。

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

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

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

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

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

相关标签: java