SimpleDateFormat为什么是线程不安全

tech2022-07-11  172

目录

前言李四的秘密李四重启之问题分析李四终成之问题落幕

前言

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 」第一时间催更

最新回复(0)