一次 SimpleDateFormat 引发的惨案(3)
- UID
- 1066743
|
一次 SimpleDateFormat 引发的惨案(3)
分析
SimpleDateFormat 是 Java 中一个相当常用的类,该类用于对日期字符串进行解析和格式化,但如果使用不当会导致非常微妙和难以调试的问题,因为它不是线程安全的,在多线程环境下调用 format() 和 parse() 方法很容易产生问题。就像上面我一旦使用 JDK8 的 parallelStream() 来遍历,它就不好使了。
“知其然,必知其所以然” 。我们来分析一下为什么会输出奇怪的“穿越”日期。
我们打开 Dash 来查阅一下 JDK 文档 对于 SimpleDateFormat 的描述:
下面通过源码来看看为什么 SimpleDateFormat 和 DateFormat 类不是线程安全的真正原因:
SimpleDateFormat 继承自 DateFormat,在 DateFormat 中定义了一个 protected 属性的 Calendar 类对象:calendar。因为 Calendar 类牵扯到了时区与本地化,JDK 的实现中使用了成员变量来传递参数,这就造成在多线程的时候会出现错误。
在 format() 方法里,有这样一段代码:
private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) {
// Convert input date to time field list
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;
}
复制代码
calendar.setTime(date) 这条语句改变了 calendar ,然后,calendar 还在 subFormat() 方法里被用到,而这就是引发问题的根源。想象一下,在一个多线程环境下,有两个线程持有了同一个SimpleDateFormat 的实例,分别调用format方法:
线程1调用 format 方法,改变了 calendar 这个字段。
中断来了。
线程2开始执行,它也改变了 calendar。
又中断了。
线程1回来了,此时,calendar 已然不是它所设的值,而是走上了线程2设计的道路。如果多个线程同时争抢 calendar 对象,则会出现各种问题。比如时间不对,线程挂死等等。
分析一下 format() 的实现,我们不难发现,用到成员变量 calendar,唯一的好处,就是在调用 subFormat() 时,少了一个参数,却带来了这许多的问题。其实,只要在这里用一个局部变量,一路传递下去,所有问题都将迎刃而解。 |
|
|
|
|
|