美文网首页面试精选
SimpleDateFormat多线程下的安全性问题

SimpleDateFormat多线程下的安全性问题

作者: 名字是乱打的 | 来源:发表于2021-09-05 22:41 被阅读0次

    背景:
    最近又看到乱用SimpleDateFormat的情况,这里做个关于SimpleDateFormat多线程下的安全性问题的总结.
    之前部门集合了一个时间工具类供大家使用,里面各式各样时间格式化的方法有几十上百个样子,然后由于很多方法都用的一个SimpleDateFormat,部门的机灵鬼发现这他娘不是重复代码嘛?然后就把他提出来了,提出来后后面也没发现什么问题,直到很久以后部门来了一个大流量的爬虫任务需要并发处理task,然后频繁调用时间格式化工具,然后在用这个SimpleDateFormat时候终于出现了问题,很多时间生成错乱,甚至根本不是一个时间的样子,或者直接报错了.

    1.问题复现

    1.1模拟并发使用SimpleDateFormat
    public class TimeConcurrErrorTest {
        static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
        public static void main(String[] args) {
            for (int i = 0; i <100 ; ++i) {
                Thread thread = new Thread(()-> {
                        try {
                            System.out.println(sdf.parse("2020-12-11 11:17:27"));
                        } catch (ParseException e) {
                            e.printStackTrace();
                        }
                });
                thread.start();
            }
        }
    }
    
    1.2问题浮现

    image 结果集如上图所示,部分时间格式转换没有出现报错,但是日期是千奇百怪的,部分调用直接报错了
    1.3问题排查
        protected Calendar calendar;
    
    // Called from Format after creating a FieldDelegate
        private StringBuffer format(Date date, StringBuffer toAppendTo,
                                    FieldDelegate delegate) {
            // Convert input date to time field list
            calendar.setTime(date);
    
            boolean useDateFormatSymbols = useDateFormatSymbols();
    
            for (int i = 0; i < compiledPattern.length; ) {
                int tag = compiledPattern[i] >>> 8;
                int count = compiledPattern[i++] & 0xff;
                if (count == 255) {
                    count = compiledPattern[i++] << 16;
                    count |= compiledPattern[i++];
                }
    
                switch (tag) {
                case TAG_QUOTE_ASCII_CHAR:
                    toAppendTo.append((char)count);
                    break;
    
                case TAG_QUOTE_CHARS:
                    toAppendTo.append(compiledPattern, i, count);
                    i += count;
                    break;
    
                default:
                    subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
                    break;
                }
            }
            return toAppendTo;
        }
    

    SimpleDateFormat(下面简称sdf)类内部有一个Calendar对象引用,它用来储存和这个sdf相关的日期信息,例如sdf.parse(dateStr), sdf.format(date) 诸如此类的方法参数传入的日期相关String, Date等等, 都是交友Calendar引用来储存的.这样就会导致一个问题,如果你的sdf是个static的, 那么多个thread 之间就会共享这个sdf, 同时也是共享这个Calendar引用, 并且, 观察 sdf.parse() 方法,你会发现有如下的调用:

    Date parse() {
     calendar.clear(); // 清理calendar
     calendar.setTime(); // 设置calendar的时间
     ... // 执行一些操作
     calendar.getTime(); // 获取calendar的时间
    }
    

    这里会导致的问题就是, 如果 线程A 调用了 sdf.parse(), 并且进行了 calendar.clear()后还未执行calendar.getTime()的时候,线程B又调用了sdf.parse(), 这时候线程B也执行了sdf.clear()方法, 这样就导致线程A的的calendar数据被清空了(实际上A,B的同时被清空了). 又或者当 A 执行了calendar.clear() 后被挂起, 这时候B 开始调用sdf.parse()并顺利i结束, 这样 A 的 calendar内存储的的date 变成了后来B设置的calendar的date.亦或是并发setTime()等等问题.
    这就造成了多线程并发修改的问题

    2.问题解决

    1.每次方法调用的时候都使用创建一个新的SimpleDateFormat自己用

    缺点:如果我们同一线程多次调用格式化方法岂不是创建销毁了很多次SimpleDateFormat?? 并发下一点点资源的损耗都会造成积少成多的情况,所以我们尽量减少重复资源的占用.这种方案可行但是不太好

    2.对于单一线程频繁使用SimpleDateFormat的,可以使用ThreadLocal存储用时再取即可

    3.使用java8提供的更安全的LocalDateTime (推荐!)

    核心思想:基于领域模型驱动设计方法以及不可变类,提供了各种各样的安全类,比如做时间差的Duration,还有LocalDate,LocalTime,LocalDateTime等不可变类,并提供了相互的转换方法

    优点:

    • 1.date有的LocalDateTime都有,有非常非常强大的Api,我也基于他的api封装了一些工具类,但是公司代码不好提供,大家可以直接参阅文档
    • 2.安全可靠

    相关文章

      网友评论

        本文标题:SimpleDateFormat多线程下的安全性问题

        本文链接:https://www.haomeiwen.com/subject/mpmswltx.html