原文:https://dzone.com/articles/java-simpledateformat-is-not-simple
格式化和解析日期是一项日常(且痛苦)的工作,每天都会带来一些额外的令人头疼的麻烦。
Java中一个普遍的格式化和解析日期的方法是使用SimpleDateFormat类。下面是一个常见的我们会用到类:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public final class DateUtils{
public static final SimpleDateFormatSIMPLE_DATE_FORMAT=newSimpleDateFormat("yyyy-MM-dd");
private DateUtils() {}
public static Date parse(String target) {
try {
return SIMPLE_DATE_FORMAT.parse(target);
} catch (ParseException e) {
e.printStackTrace();
}
return null;
}
public static String format(Date target) {
returnSIMPLE_DATE_FORMAT.format(target);
}
}
你认为它会按照我们期望的那样起作用吗?我们来试一下:
private static void testSimpleDateFormatInSingleThread() {
final String source="2019-01-11";
System.out.println(DateUtils.parse(source));
}
// Fri Jan 11 00:00:00 IST 2019
很好,它确实有效。再让我们用多线程来测试一下。
private static void testSimpleDateFormatWithThreads() {
ExecutorService executorService=Executors.newFixedThreadPool(10);
final String source="2019-01-11";
System.out.println(":: parsing date string ::");
IntStream.rangeClosed(0,20)
.forEach((i)->executorService.submit(()->System.out.println(DateUtils.parse(source))));
executorService.shutdown();
}
得到的结果如下:
:: parsing date string ::
... omitted
Fri Jan 11 00:00:00 IST 2019Sat Jul 11 00:00:00 IST 2111Fri Jan 11 00:00:00 IST 2019... omitted
这结果蛮有意思的,对吧?这是一个我们大多数在Java中格式化日期时都犯过的很常见的错误。为什么呢?
因为我们没有考虑到线程安全的问题。下面是Java doc中对于simpleDateFormat的描述:
"Date formats are not synchronized.
It is recommended to create separate format instances for each thread.
If multiple threads access a format concurrently, it must be synchronized
externally."
Tip:当我们使用实例变量时,应该总是要检查它们是否是线程安全的。
如文档所说,我们可以通过给每个线程使用单独的实例来解决这个问题。但如果我们想共享一个实例呢?如何来解决?
方案1:ThreadLocal
使用ThreadLocal变量可以解决这个问题。ThreadLocal的get()方法会给我们当前线程的正确的值。
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public final class DateUtilsThreadLocal{
public static final ThreadLocal SIMPLE_DATE_FORMAT=ThreadLocal.withInitial(()->newSimpleDateFormat("yyyy-MM-dd"));
privateDateUtilsThreadLocal() {}
public static Date parse(String target) {
try{
return SIMPLE_DATE_FORMAT.get().parse(target);
}catch(ParseException e) {
e.printStackTrace();
}
return null;
}
public static String format(Date target) {
return SIMPLE_DATE_FORMAT.get().format(target);
}
}
方案2:Java8 线程安全的日期-时间 API
Java8带来了一个新的日期-时间API,从而我们有了一个更好的,问题更少的SimpleDateFormate的替代品。如果我们实在是需要坚持使用SimpleDateFormat,我们可以继续使用ThreadLocal。但是当我们有了一个更好的选择时,我们应该好好考虑要不要使用它。
Java8带来了不少线程安全日期类。
Java 文档中这样说:
"This class is immutable and thread-safe."
多学习一下这些类是很有价值的,包括了DateTimeFormatter, OffsetDateTime, ZonedDateTime, LocalDateTime, LocalDate, 和 LocalTime。
我们的解决办法:
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class DateUtilsJava8{
public static final DateTimeFormatter DATE_TIME_FORMATTER=DateTimeFormatter.ofPattern("yyyy-MM-dd");
private DateUtilsJava8() {}
public static LocalDate parse(String target) {
return LocalDate.parse(target,DATE_TIME_FORMATTER);
}
public static String format(LocalDate target) {
return target.format(DATE_TIME_FORMATTER);
}
}
结论:Java8的解决方案使用了一个不可变类,不可变类对于解决多线程问题是一个很好地实践。不可变类天生就是线程安全的,所以尽可能多的使用它们。
Happy coding!