为什么多线程下不建议使用SimpleDateFormat 前

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

前言

经常听到开发大佬说不建议用SimpleDateFormat,包括阿里巴巴java开发手册也写了“SimpleDateFormat是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用 DateUtils 工具类。”

image.png
可是,为什么说SimpleDateFormat是线程不安全的类呢?要想证明这个问题,首先要聚一个例子,看看在多线程环境下,使用SimpleDateFormat会怎么样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typescript复制代码public static void main(String[] args) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
CountDownLatch countDownLatch = new CountDownLatch(30);
for (int i = 0; i < 30; i++) {
CompletableFuture.runAsync(() -> {
//先获取当前时间的格式化字符串
String str1 = simpleDateFormat.format(new Date());
try {
//在通过格式化字符串再操作一次
Date parseDate = simpleDateFormat.parse(str1);
String str2 = simpleDateFormat.format(parseDate);
//对比前后格式化字符串是否一致
System.out.println(String.format("threadId = %s , 前 = %s , 后 = %s", Thread.currentThread().getId(), str1, str2));
} catch (Exception ignored) {
} finally {
countDownLatch.countDown();
}
}, ThreadPoolUtil.pool);
}
countDownLatch.await();
}

在同个一个线程中,多次操作同一个时间new Date(),比较前后操作的结果是否一样。一样,则代表是现场安全的。

在看一下运行的结果

image.png

发现确实在某些线程中,前后的时间不一致。那就说明SimpleDateFormat确实是线程不安全的。

看一下format方法的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ini复制代码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,并修改calendar。因此在多线程环境下,当多个线程同时使用相同的SimpleDateFormat对象(如static修饰)的话,如调用format方法时,多个线程会同时调用calender.setTime方法,导致time被别的线程修改,因此线程是不安全的。

此外,parse方法也是线程不安全的,parse方法实际调用的是CalenderBuilder的establish来进行解析,其方法中主要步骤不是原子操作。
想要避免,要不就加锁、要不就在方法内new SimpleDateFormat。不管是哪种,都是对性能有所影响的。JDK8的DateTimeFormatter是一个不错的解决方式。我们来看看DateTimeFormatter是怎么保证线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
swift复制代码public final class DateTimeFormatter {

/**
* The printer and/or parser to use, not null.
*/
private final CompositePrinterParser printerParser;
/**
* The locale to use for formatting, not null.
*/
private final Locale locale;
/**
* The symbols to use for formatting, not null.
*/
private final DecimalStyle decimalStyle;
/**
* The resolver style to use, not null.
*/
private final ResolverStyle resolverStyle;
/**
* The fields to use in resolving, null for all fields.
*/
private final Set<TemporalField> resolverFields;
/**
* The chronology to use for formatting, null for no override.
*/
private final Chronology chrono;
/**
* The zone to use for formatting, null for no override.
*/
private final ZoneId zone;

DateTimeFormatter的类是用final修饰的,成员变量也是用final修饰的。这就代表DateTimeFormatter是个不可变的对象,意味着线程B对象无法修改线程A的DateTimeFormatter对象中的变量。线程B只能自己新建一个DateTimeFormatter对象。

总结

简述了SimpleDateFormat为什么是线程不安全的和DateTimeFormatter为什么是线程安全的之后,我们在实际开发中,要努力去规范我们的代码,在使用封装好的工具类时,也要知其原理,避免问题。

如果项目是JDK8的话,在处理时间问题时,不妨直接用DateTimeFormatter来试试!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%