多线程SimpleDateFormat日期格式化与ThreadLocal用法

前言

SimpleDateFormat 是 JDK 提供的一个日期格式化和解析类,但它是非线程安全的,原因如下。

1. parse方法

        Date parsedDate;
        try {
            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;
        }

其中,calb.establish(calendar)初始化了calendar日历数据

跟进查看

        //1-----------------------清空
        cal.clear();
        // Set the fields from the min stamp to the max stamp so that
        // the field resolution works in the Calendar.
        for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
            for (int index = 0; index <= maxFieldIndex; index++) {
                if (field[index] == stamp) {
                    cal.set(index, field[MAX_FIELD + index]);//2--------------设置
                    break;
                }
            }
        }

        if (weekDate) {
            int weekOfYear = isSet(WEEK_OF_YEAR) ? field[MAX_FIELD + WEEK_OF_YEAR] : 1;
            int dayOfWeek = isSet(DAY_OF_WEEK) ?
                                field[MAX_FIELD + DAY_OF_WEEK] : cal.getFirstDayOfWeek();
            if (!isValidDayOfWeek(dayOfWeek) && cal.isLenient()) {
                if (dayOfWeek >= 8) {
                    dayOfWeek--;
                    weekOfYear += dayOfWeek / 7;
                    dayOfWeek = (dayOfWeek % 7) + 1;
                } else {
                    while (dayOfWeek <= 0) {
                        dayOfWeek += 7;
                        weekOfYear--;
                    }
                }
                dayOfWeek = toCalendarDayOfWeek(dayOfWeek);
            }
            cal.setWeekDate(field[MAX_FIELD + WEEK_YEAR], weekOfYear, dayOfWeek);//2-----------设置
        }

        //3-------------------返回
        return cal;

参考1- 2- 3-注释,线程不安全的原因,不能保证多核模式下,多线程执行是顺序执行的原子性

2. format方法源码

/**
     * Formats the given Date into a date/time string and appends
     * the result to the given StringBuffer.
     *
     * @param date the date-time value to be formatted into a date-time string.
     * @param toAppendTo where the new date-time text is to be appended.
     * @param pos the formatting position. On input: an alignment field,
     * if desired. On output: the offsets of the alignment field.
     * @return the formatted date-time string.
     * @exception NullPointerException if the given {@code date} is {@code null}.
     */
    @Override
    public StringBuffer format(Date date, StringBuffer toAppendTo,
                               FieldPosition pos)
    {
        pos.beginIndex = pos.endIndex = 0;
        return format(date, toAppendTo, pos.getFieldDelegate());
    }

    // 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;
    }

//设置日期

calendar.setTime(date); 

//格式化时间

subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);

calendar是有状态的,不能保证原子性。

3. 解决方法

3.1. 加锁

自建方法包裹代码


public class SimpleDateFormatTest {

    
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static synchronized Date parse(String s) throws ParseException {
        return sdf.parse(s);
    }

    public static void main(String[] args) {
        
        for (int i = 0; i <20 ; ++i) {
            Thread thread = new Thread(() -> {
                public void run() {
                    try {
                        
                            System.out.println(parse("2018-07-17 10:52:16"));
                        
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            };
            thread.start()
        }
    }

}

串行执行,在大批量调用效率很低,不推荐

3.2.每次调用创建一个对象

    public Date parse(String s) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.parse(s);
    }

缺点,耗内存,GC压力增大,每次创建对象销毁,分配和销毁内存空间,整理内存,GC。不推荐

3.3.每个线程创建一个对象

每个线程带一个ThreadLocalMap对象,以threadlocal对象为key,传递变量为值。每个线程仅修改当前线程的副本。

public class SimpleDateFormatTest {
    
    static ThreadLocal local= new ThreadLocal(){
        @Override
        protected SimpleDateFormat initialValue(){
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static void main(String[] args) {
        
        for (int i = 0; i < 5; ++i) {
            Thread thread = new Thread(() -> {
                public void run() {
                    try {
                        System.out.println(local.get().parse("2018-07-17 11:01:16"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }finally {
                        //TODO 线程运行结束清除,避免内存泄露
                        local.remove();
                    }
                }
            });

            thread.start();
        }
    }

}

4. 总结 

推荐使用ThreadLocal,每个线程一个对象,提高format对象利用率

注意:使用ThreadLocal的Thread均持有ThreadLocal自定义的map对象,key是ThreadLocal对象的弱引用,随ThreadLocal的GC或者内存不足就会GC,value是我们创建的对象是强引用,可能一直存在。因此ThreadLocal调用结束后,需要显示调用remove()方法

你可能感兴趣的:(Java,多线程线程安全)