SimpleDateFormat多线程下的安全性问题

背景:
最近又看到乱用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多线程下的安全性问题)