SimpleDateFormat为什么是线程不安全

前言

SimpleDateFormat是 Java 提供的个格式化和解析日期的工具类,但是你是否在夜深人静的时候想过,自己通过SimpleDateFormat格式化日期的时候会不会出现线程安全方面的问题呢?

在这里插入图片描述

李四就是这样,清晨,第二天顶着两个硕大的黑眼圈写起了bug,不不不,是写起来代码。正所谓怕什么来什么,尽管李四昨晚一夜的祈祷不要代码出现问题,但始终没能避免王二越来越近的步伐…
在这里插入图片描述

李四的秘密

王二对着李四说道:
阿四啊,你最近干的不错,但是你有没有发现关于SimpleDateFormat的使用这个地方有问题呢

报告长官,暂时没有发现情况!

代码:
class TestSimpleDateFormat {
    //创建单例实例 1
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        //创建多个线程2
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    System.out.println(sdf.parse("2020-9-9 11:11:11"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
                //线程启动 3
            }).start();
        }
    }
}

王二:
阿四呀,我可是特意做了多线程处理,你居然还没发现问题,要是7米,早就一眼看出问题了,巴拉巴拉…

李四越听王二讲话,心里越不踏实,豆大的汗珠从额头冒了出来,在听到如果再不仔细干,绩效会被扣的时候,心里更是一紧,恐惧开始从李四的心底蔓延开来,于是李四为了不在发生这样的情况,他做出了一个重大的决定,周末约同事小红去动物园看老虎…

在这里插入图片描述
代码运行结果:
在这里插入图片描述

李四重启之问题分析

SimpleDateFormat类图结构
在这里插入图片描述
每个SimpleDateFormat实例里面都有一个Calendar对象。SimpleDateFormat之所以是线程不安全的, 是因为 Calendar 线程不安全 。后者之所以是线程不安全的,是因为其中存放日期数据的变量都是线程不安全的,比如 fields、time 等。

parse方法源码 :

public Date parse(String text, ParsePosition pos)
    {
   		//1 解析日期字符串,并将解析好的数据放入CalendarBuilder的实例 calb中
   		...
        Date parsedDate;
        try {
        	//2 使用calb中解析好的日期数据设置calendar
            parsedDate = calb.establish(calendar).getTime();
            // If the year value is ambiguous,
            // then the two-digit year == the default start year
            if (ambiguousYear[0]) {
                if (parsedDate.before(defaultCenturyStart)) {
                    parsedDate = calb.addYear(100).establish(calendar).getTime();
                }
            }
        }
        // An IllegalArgumentException will be thrown by Calendar.getTime()
        // if any fields are out of range, e.g., MONTH == 17.
        catch (IllegalArgumentException e) {
            pos.errorIndex = start;
            pos.index = oldStart;
            return null;
        }

        return parsedDate;
    }

establish方法源码:

Calendar establish(Calendar cal) {
	//3 重置日期对象cal的属性值
    cal.clear();
	//4 使用 calb中的属性值设置cal
	...
	//5 返回设置好的 cal 对象
    return cal;
}

clear方法源码:

public final void clear()
 {
     for (int i = 0; i < fields.length; ) {
         stamp[i] = fields[i] = 0; // UNSET == 0
         isSet[i++] = false;
     }
     areAllFieldsSet = areFieldsSet = false;
     isTimeSet = false;
 }

从以上代码可以看出, 代码3、4、5不是原子性操作,当多个线程调用parse方法时,操作的是同一个cal对象,可能发生情况:

(1)返回数据又被clear清空的对象
(2)设置好的cal对象又被其它线程修改


李四终成之问题落幕

李四孤独的看完老虎后,痛定思痛,进过一番严肃认真的分析…
在这里插入图片描述

1.创建局部变量

每次使用时 new 一个SimpleDateFormat 实例,这样可以保证每个实例使自己的 Calendar 实例。
缺点:每次使用都需 new一个对象 ,并且使用后由于没有其他引用, 又需要回收,开销会很大

2.加锁

class TestSimpleDateFormat {
    //创建单例实例 1
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        //创建多个线程2
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    //加锁 3
                    synchronized (sdf) {
                        System.out.println(sdf.parse("2020-9-9 11:11:11"));
                    }
                } catch (ParseException e) {
                    e.printStackTrace();
                }
                //线程启动 4
            }).start();
        }
    }
}

缺点:高并发情况下性能较差,多个线程需要竞争锁,每次需要等待锁释放


3.使用ThreadLocal(推荐)
每个线程使用一个SimpleDateFormat实例。

class TestSimpleDateFormat {
    //创建单例实例 1
    static ThreadLocal<DateFormat> sdf = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static void main(String[] args) {
        //创建多个线程2
        for (int i = 0; i < 500; i++) {
            new Thread(() -> {
                try {
                    //单例解析 3
                    System.out.println(sdf.get().parse("2020-9-9 11:11:11"));
                } catch (ParseException e) {
                    e.printStackTrace();
                } finally {
                    //清除,避免内存泄露
                    sdf.remove();
                }
                //线程启动 5
            }).start();
        }
    }
}

李四或许不在你的身边,但李四的故事会一直流传…

关注
文章持续更新,可以微信搜索「 熊猫程序猿a 」第一时间催更
公众号

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三省同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值