线程安全的 SimpleDateFormat
- Java
- 2017-10-27
- 137热度
- 0评论
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.
解决方案有三种:
- 每次都用一个新的对象执行日期格式化操作,不要在很多操作中共用一个 SimpleDateFormat 对象。很简单,缺点是效率低,如果有一百万次操作,就要生成和销毁一百万个对象。
- 给调用 format 或 parse 的外层代码加锁,确保同一时间只有一个线程执行此方法。缺点也是效率低(并发量小可以忽略,但本文讨论的是大并发的情况),线程阻塞。
- 使用 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]