SimpleDateFormat线程不安全
我们使用了static对SimpleDateFormat进行了修饰,所有线程共享,而同时SimpleDateFormat又是线程不安全的,导致多线程并发使用公用SimpleDateFormat实例对日期进行解析或者格式化出错
那么为什么SimpleDateFormat是线程不安全的呢???
SimpleDateFormat类有一个Calendar类型的成员变量,用来储存和相关的日期信息。当我们调用parse和format方法传入的日期时, 都会将该数据传入Calendar储存。那么当我们使用static修饰SimpleDateFormat时,多线程之间也会共享这个SimpleDateFormat对象的Calendar。那么就可能存在线程A修改了Calendar后,线程B又修改了Calendar,导致线程A后续使用Calendar时已不是它所修改的值,从而出现异常
我们具体看SimpleDateFormat中parse方法的实现
public Date parse(String text, ParsePosition pos)
{
//1、 解析字符串放入CalendarBuilder的实例calb中
...
Date parsedDate;
try {
//2、使用calb中解析好的日期数据设置calendar
parsedDate = calb.establish(calendar).getTime();
...
}
catch (IllegalArgumentException e) {
...
return null;
}
return parsedDate;
}
Calendar establish(Calendar cal) {
...
//3、情况日期对象cal的属性值
cal.clear();
//4、 使用calb中中属性设置cal
...
//5、返回设置好的cal对象
return cal;
}
步骤3和4操作显然不是原子性操作,当多个线程调用parse方法时。假设线程A执行了步骤3、4设置了cal对象,在执行步骤5前线程B执行了步骤3(或步骤4)清空了cal对象(修改了cal对象),由于多个线程使用的是一个cal对象,所以线程A执行步骤5返回的就可能是被线程B清空后(修改后)的对象,从而导致程序错误。
SimpleDateFormat中format方法的实现
private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) {
// 把时间保存
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;
}
假设线程A执行calendar.setTime(date),把时间设置成A后,线程B也执行到了calendar.setTime(date),把时间设置为B。线程A继续执行,subFormat方法中使用calendar时,用的已经是线程B设置的值了,从而导致出现时间不对,线程挂死等等。
解决方案
1、每次使用,重新new一个SimpleDateFormat对象。不建议使用,频繁地创建和销毁对象,效率较低。
2、使用synchronized,简单粗暴,并发量大的时候会对性能有影响,线程阻塞。
3、使用ThreadLocal
4、java8可以使用DateTimeFormatter
那么为什么说DateTimeFormatter是线程安全的呢
每次调用DateTimeFormmater的format方法时,会创建一个DateTimePrintContext对象用来存储传入的时间信息,之后对于时间的格式转换会基于该context数据进行,那么多对于不同线程来说,他们只能看见属于自己的时间信息,当然是线程安全的。具体代码如下
public String format(TemporalAccessor temporal) {
StringBuilder buf = new StringBuilder(32);
formatTo(temporal, buf);
return buf.toString();
}
public void formatTo(TemporalAccessor temporal, Appendable appendable) {
Objects.requireNonNull(temporal, "temporal");
Objects.requireNonNull(appendable, "appendable");
try {
//每个线程都会创建新的context
DateTimePrintContext context = new DateTimePrintContext(temporal, this);
//根据context对时间进行转换
if (appendable instanceof StringBuilder) {
printerParser.format(context, (StringBuilder) appendable);
} else {
// buffer output to avoid writing to appendable in case of error
StringBuilder buf = new StringBuilder(32);
printerParser.format(context, buf);
appendable.append(buf);
}
} catch (IOException ex) {
throw new DateTimeException(ex.getMessage(), ex);
}
}
那么对于parse方法呢?
parse方法中存在一个DateTimeParseContext上下文,对于传入的时间数据都会解析到DateTimeParseContext对象中进行存储,每次调用都会新创建一个DateTimeParseContext对象
public static LocalDateTime parse(CharSequence text, DateTimeFormatter formatter) {
Objects.requireNonNull(formatter, "formatter");
return formatter.parse(text, LocalDateTime::from);
}
public <T> T parse(CharSequence text, TemporalQuery<T> query) {
Objects.requireNonNull(text, "text");
Objects.requireNonNull(query, "query");
try {
return parseResolved0(text, null).query(query);
} catch (DateTimeParseException ex) {
throw ex;
} catch (RuntimeException ex) {
throw createError(text, ex);
}
}
private TemporalAccessor parseResolved0(final CharSequence text, final ParsePosition position) {
ParsePosition pos = (position != null ? position : new ParsePosition(0));
//解析传入时间数据到context中
DateTimeParseContext context = parseUnresolved0(text, pos);
//这里对text数据进行处理,只是进行日志打印,对实际解析无实质影响
if (context == null || pos.getErrorIndex() >= 0 || (position == null && pos.getIndex() < text.length())) {
String abbr;
if (text.length() > 64) {
abbr = text.subSequence(0, 64).toString() + "...";
} else {
abbr = text.toString();
}
if (pos.getErrorIndex() >= 0) {
throw new DateTimeParseException("Text '" + abbr + "' could not be parsed at index " +
pos.getErrorIndex(), text, pos.getErrorIndex());
} else {
throw new DateTimeParseException("Text '" + abbr + "' could not be parsed, unparsed text found at index " +
pos.getIndex(), text, pos.getIndex());
}
}
return context.toResolved(resolverStyle, resolverFields);
}