SimpleDateFormat线程不安全

我们使用了static对SimpleDateFormat进行了修饰,所有线程共享,而同时SimpleDateFormat又是线程不安全的,导致多线程并发使用公用SimpleDateFormat实例对日期进行解析或者格式化出错

image-20200113114304251.png

那么为什么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,简单粗暴,并发量大的时候会对性能有影响,线程阻塞。

image-20200113114440501.png

3、使用ThreadLocal

image-20200113114830092.png

4、java8可以使用DateTimeFormatter

image-20200113115009817.png
那么为什么说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);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值