欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SimpleDateFormat线程不安全

程序员文章站 2024-03-15 16:19:30
...

我们使用了static对SimpleDateFormat进行了修饰,所有线程共享,而同时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,简单粗暴,并发量大的时候会对性能有影响,线程阻塞。

SimpleDateFormat线程不安全

3、使用ThreadLocal

SimpleDateFormat线程不安全

4、java8可以使用DateTimeFormatter

SimpleDateFormat线程不安全
那么为什么说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);
}
相关标签: java