前言
经常听到开发大佬说不建议用SimpleDateFormat,包括阿里巴巴java开发手册也写了“SimpleDateFormat是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用 DateUtils 工具类。”
可是,为什么说SimpleDateFormat是线程不安全的类呢?要想证明这个问题,首先要聚一个例子,看看在多线程环境下,使用SimpleDateFormat会怎么样。
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(),比较前后操作的结果是否一样。一样,则代表是现场安全的。
在看一下运行的结果
发现确实在某些线程中,前后的时间不一致。那就说明SimpleDateFormat确实是线程不安全的。
看一下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,并修改calendar。因此在多线程环境下,当多个线程同时使用相同的SimpleDateFormat对象(如static修饰)的话,如调用format方法时,多个线程会同时调用calender.setTime方法,导致time被别的线程修改,因此线程是不安全的。
此外,parse方法也是线程不安全的,parse方法实际调用的是CalenderBuilder的establish来进行解析,其方法中主要步骤不是原子操作。 想要避免,要不就加锁、要不就在方法内new SimpleDateFormat。不管是哪种,都是对性能有所影响的。JDK8的DateTimeFormatter是一个不错的解决方式。我们来看看DateTimeFormatter是怎么保证线程安全的。
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来试试!