线程安全的 SimpleDateFormat

SimpleDateFormat 不是线程安全的。如最常用的 parse 和 format 方法,在大并发情况下,会出现异常。

如文档所述:

* Date formats are not synchronized.
* It is recommended to create separate format instances for each thread.
* If multiple threads access a format concurrently, it must be synchronized
* externally.

解决方案有三种:

  1. 每次都用一个新的对象执行日期格式化操作,不要在很多操作中共用一个 SimpleDateFormat 对象。很简单,缺点是效率低,如果有一百万次操作,就要生成和销毁一百万个对象。
  2. 给调用 format 或 parse 的外层代码加锁,确保同一时间只有一个线程执行此方法。缺点也是效率低(并发量小可以忽略,但本文讨论的是大并发的情况),线程阻塞。
  3. 使用 ThreadLocal,自动为每个线程生成一个单独对象。

我觉得最佳方案是第三种。

找到一个使用 ThreadLocal 实现的 SimpleDateFormat 封装类,使用方法与 SimpleDateFormat 一样,却是线程安全的,可以在项目中直接替代 SimpleDateFormat。

代码如下:

import java.text.AttributedCharacterIterator;
import java.text.DateFormatSymbols;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

public class SimpleDateFormatThreadSafe extends SimpleDateFormat {

    private static final long serialVersionUID = 5448371898056188202L;
    ThreadLocal localSimpleDateFormat;
    private String pattern;

    public SimpleDateFormatThreadSafe() {
        super();
        localSimpleDateFormat = new ThreadLocal() {
            protected SimpleDateFormat initialValue() {
                if (pattern != null) {
                    return new SimpleDateFormat(pattern);
                }
                return new SimpleDateFormat();
            }
        };
    }

    public SimpleDateFormatThreadSafe(final String pattern) {
        super(pattern);
        this.pattern = pattern;
        localSimpleDateFormat = new ThreadLocal() {
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat(pattern);
            }
        };
    }

    public SimpleDateFormatThreadSafe(final String pattern, final DateFormatSymbols formatSymbols) {
        super(pattern, formatSymbols);
        this.pattern = pattern;
        localSimpleDateFormat = new ThreadLocal() {
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat(pattern, formatSymbols);
            }
        };
    }

    public SimpleDateFormatThreadSafe(final String pattern, final Locale locale) {
        super(pattern, locale);
        this.pattern = pattern;
        localSimpleDateFormat = new ThreadLocal() {
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat(pattern, locale);
            }
        };
    }

    public Object parseObject(String source) throws ParseException {
        return localSimpleDateFormat.get().parseObject(source);
    }

    public String toString() {
        return localSimpleDateFormat.get().toString();
    }

    public Date parse(String source) throws ParseException {
        return localSimpleDateFormat.get().parse(source);
    }

    public Object parseObject(String source, ParsePosition pos) {
        return localSimpleDateFormat.get().parseObject(source, pos);
    }

    public void setCalendar(Calendar newCalendar) {
        localSimpleDateFormat.get().setCalendar(newCalendar);
    }

    public Calendar getCalendar() {
        return localSimpleDateFormat.get().getCalendar();
    }

    public void setNumberFormat(NumberFormat newNumberFormat) {
        localSimpleDateFormat.get().setNumberFormat(newNumberFormat);
    }

    public NumberFormat getNumberFormat() {
        return localSimpleDateFormat.get().getNumberFormat();
    }

    public void setTimeZone(TimeZone zone) {
        localSimpleDateFormat.get().setTimeZone(zone);
    }

    public TimeZone getTimeZone() {
        return localSimpleDateFormat.get().getTimeZone();
    }

    public void setLenient(boolean lenient) {
        localSimpleDateFormat.get().setLenient(lenient);
    }

    public boolean isLenient() {
        return localSimpleDateFormat.get().isLenient();
    }

    public void set2DigitYearStart(Date startDate) {
        localSimpleDateFormat.get().set2DigitYearStart(startDate);
    }

    public Date get2DigitYearStart() {
        return localSimpleDateFormat.get().get2DigitYearStart();
    }

    public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) {
        return localSimpleDateFormat.get().format(date, toAppendTo, pos);
    }

    public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
        return localSimpleDateFormat.get().formatToCharacterIterator(obj);
    }

    public Date parse(String text, ParsePosition pos) {
        return localSimpleDateFormat.get().parse(text, pos);
    }

    public String toPattern() {
        return localSimpleDateFormat.get().toPattern();
    }

    public String toLocalizedPattern() {
        return localSimpleDateFormat.get().toLocalizedPattern();
    }

    public void applyPattern(String pattern) {
        localSimpleDateFormat.get().applyPattern(pattern);
        this.pattern = pattern;
    }

    public void applyLocalizedPattern(String pattern) {
        localSimpleDateFormat.get().applyLocalizedPattern(pattern);
        this.pattern = pattern;
    }

    public DateFormatSymbols getDateFormatSymbols() {
        return localSimpleDateFormat.get().getDateFormatSymbols();
    }

    public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) {
        localSimpleDateFormat.get().setDateFormatSymbols(newFormatSymbols);
    }

    public Object clone() {
        return localSimpleDateFormat.get().clone();
    }

    public int hashCode() {
        return localSimpleDateFormat.get().hashCode();
    }

    public boolean equals(Object obj) {
        return localSimpleDateFormat.get().equals(obj);
    }

}

来自 https://gist.github.com/pablomoretti/9748230,有改动。

我加了成员变量 pattern,主要用来记录是否设置 pattern,如果设置了,调用默认构造函数时都加上 pattern。

为什么要这样做?我在 tomcat 环境下调试一段程序,设置 pattern 时(线程A),localSimpleDateFormat.get() 拿到的对象地址为 f67a0200,调用 format 方法时(线程B,ThreadLocal 会调用一次 SimpleDateFormat 的默认构造函数,没有指定 pattern),localSimpleDateFormat.get() 拿到的对象地址为 b5341f2a。对象不一致,所以 format 的结果不符合预期。修改之后,可以解决这个问题。

 

参考 StackOverFlow:Why is Java's SimpleDateFormat not thread-safe? [duplicate]