省流:SimpleDateFormat线程不安全,性能也低;DateTimeFormatter支持的是java8推出的LocalDateTime,性能和线程安全方面较好;FastDateFormat是Apache的实现,需要给项目增加依赖,对Date类型友好,性能和线程安全较好。
=====以下是正文=====
时间格式化是日常java开发中比较常见的一种操作。时间格式化器的实现有很多种,可能大家在平时的编码过程中用的比较多的是SimpleDateFormat。但是你了解这种实现所隐含的“坑”吗?本文将对比SimpleDateFormat、DateTimeFormatter和FastDateFormat三种实现,从并发安全和性能两个角度来测试它们的优缺点,帮助大家更好地使用时间格式化器。
话不多说,直接上代码。(代码是基于jupiter编写的单元测试类,为了分段说明,存在一些省略)
基础代码:
// 测试类
public class DateFormatTest {
//定义测试方法运行超时时间
private final static long TIME_LIMIT=10;
//定义格式
private final static String PATTERN="yyyy-MM-dd HH:mm:ss";
//性能测试运行次数
private static final int TIMES =10_000_000;
//定义随机日期产生的范围
private static final long rangeMin;
private static final long rangeMax;
static {
Date min = DateUtil.parse("2000-01-01 00:00:00");
Date max = DateUtil.parse("2030-12-31 23:59:59");
rangeMin = min.getTime();
rangeMax = max.getTime();
}
/**
* 在一定时间范围内生成随机日期
* @return
*/
private Date randomDate(){
long randTimestamp = RandomUtil.randomLong(rangeMin,rangeMax);
return new Date(randTimestamp);
}
}
SimpleDateFormat线程安全测试:
@Test
@Timeout(TIME_LIMIT)
public void simpleDateFormatConcurrentSafeTest(){
//利用线程池进行并发格式化方法调用
int capacity=32;
BlockingQueue<Runnable> workingQueue=new ArrayBlockingQueue<>(capacity);
ThreadPoolExecutor threadPool=new ThreadPoolExecutor(capacity,capacity,0, TimeUnit.SECONDS,workingQueue);
//所有线程公用这个格式化器
SimpleDateFormat sdf=new SimpleDateFormat(PATTERN);
CountDownLatch latch=new CountDownLatch(1);
for (int i=1;i<=capacity;i++){
String threadNo="T-"+i;
threadPool.execute(() -> {
while(true){
try{
String dateStr = sdf.format(randomDate());
System.out.println(threadNo+":"+dateStr);
}catch (Exception e){
System.err.println(threadNo);
e.printStackTrace();
System.exit(1);
}
}
});
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程结束:SimpleDateFormat");
}
这段代码在短暂运行后会抛出如下异常
java.lang.ArrayIndexOutOfBoundsException: Index 265 out of bounds for length 13
at java.base/sun.util.calendar.BaseCalendar.getCalendarDateFromFixedDate(BaseCalendar.java:453)
at java.base/java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2394)
at java.base/java.util.GregorianCalendar.computeFields(GregorianCalendar.java:2309)
at java.base/java.util.Calendar.complete(Calendar.java:2301)
at java.base/java.util.Calendar.get(Calendar.java:1856)
at java.base/java.text.SimpleDateFormat.subFormat(SimpleDateFormat.java:1150)
at java.base/java.text.SimpleDateFormat.format(SimpleDateFormat.java:997)
at java.base/java.text.SimpleDateFormat.format(SimpleDateFormat.java:967)
at java.base/java.text.DateFormat.format(DateFormat.java:374)
at DateFormatTest.lambda$simpleDateFormatConcurrentSafeTest$0(DateFormatTest.java:67)
而该异常在单线程情况下是不会出现的,这说明SimpleDateFormat实例不宜被多个线程复用。
DateTimeFormatter线程安全测试:
@Test
@Timeout(TIME_LIMIT)
public void localDateConcurrentSafeTest(){
int capacity=32;
BlockingQueue<Runnable> workingQueue=new ArrayBlockingQueue<>(capacity);
ThreadPoolExecutor threadPool=new ThreadPoolExecutor(capacity,capacity,0, TimeUnit.SECONDS,workingQueue);
DateTimeFormatter formatter=DateTimeFormatter.ofPattern(PATTERN);
ZoneId zoneId=ZoneId.systemDefault();
CountDownLatch latch=new CountDownLatch(1);
for (int i=1;i<=capacity;i++){
String threadNo="T-"+i;
threadPool.execute(() -> {
while(true){
try{
String dateStr = formatter.format(randomDate().toInstant().atZone(zoneId).toLocalDateTime());
System.out.println(threadNo+":"+dateStr);
}catch (Exception e){
System.err.println(threadNo);
e.printStackTrace();
System.exit(1);
}
}
});
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程结束:DateTimeFormatter");
}
程序直到运行超时,未出现任何异常。说明DateTimeFormatter是一种线程安全的格式化器。值得注意的是,该实现的直接作用对象是LocalDateTime(since jdk1.8)实例。如果项目上大量使用的是Date类型,可能需要一些适配工作。
FastDateFormat线程安全测试:
@Test
@Timeout(TIME_LIMIT)
public void fastDateFormatConcurrentSafeTest(){
int capacity=32;
BlockingQueue<Runnable> workingQueue=new ArrayBlockingQueue<>(capacity);
ThreadPoolExecutor threadPool=new ThreadPoolExecutor(capacity,capacity,0, TimeUnit.SECONDS,workingQueue);
FastDateFormat fastDateFormat=FastDateFormat.getInstance(PATTERN);
CountDownLatch latch=new CountDownLatch(1);
for (int i=1;i<=capacity;i++){
String threadNo="T-"+i;
threadPool.execute(() -> {
while(true){
try{
String dateStr = fastDateFormat.format(randomDate());
System.out.println(threadNo+":"+dateStr);
}catch (Exception e){
System.err.println(threadNo);
e.printStackTrace();
System.exit(1);
}
}
});
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("FastDateFormat:主线程结束");
}
程序直到运行超时,未出现任何异常。说明FastDateFormat是一种线程安全的格式化器。
下面是性能测试环节。每种格式化器会循环执行一千万次格式化方法
SimpleDateFormat性能测试:
@Test
public void simpleDateFormatBenchmark() throws ParseException {
long start=System.currentTimeMillis();
SimpleDateFormat sdf=new SimpleDateFormat(PATTERN);
for (int i = 0; i< TIMES; i++){
String dateStr=sdf.format(randomDate());
sdf.parse(dateStr);
}
long end=System.currentTimeMillis();
System.out.println("SimpleDateFormat耗时:"+(end-start));
}
执行耗时14880ms。
DateTimeFormatter性能测试:
@Test
public void localDateBenchmark(){
long start=System.currentTimeMillis();
DateTimeFormatter formatter=DateTimeFormatter.ofPattern(PATTERN);
ZoneId zoneId=ZoneId.systemDefault();
for (int i = 0; i< TIMES; i++){
String dateStr=formatter.format(randomDate().toInstant().atZone(zoneId).toLocalDateTime());
formatter.parse(dateStr);
}
long end=System.currentTimeMillis();
System.out.println("DateTimeFormatter执行"+TIMES+",耗时:"+(end-start));
}
执行耗时8895ms。
FastDateFormat性能测试:
@Test
public void fastDateFormatBenchmark() throws ParseException {
long start=System.currentTimeMillis();
FastDateFormat fastDateFormat=FastDateFormat.getInstance(PATTERN);
for (int i = 0; i< TIMES; i++){
String dateStr=fastDateFormat.format(randomDate());
fastDateFormat.parse(dateStr);
}
long end=System.currentTimeMillis();
System.out.println("FastDateFormat耗时:"+(end-start));
}
执行耗时10952ms。
可以看出,在性能表现上,DateTimeFormatter>FastDateFormat>>SimpleDateFormat。考虑到DateTimeFormatter还需要进行Date到LocalDateTime的转化,如果项目上能够广泛使用LocalDateTime来记录时间信息,那么DateTimeFormatter无疑是最优解。
最后总结一下。
SimpleDateFormat无论在线程安全还是性能上都不理想。作为jdk1.1时代的元老级api,如果仅仅是编写一些测试代码,用它无妨。但是在生产中要谨慎使用,甚至避免使用。
DateTimeFormatter满足线程安全要求,且性能表现最好,并且也是jdk自带的api。项目如果已经在广泛使用LocalDateTime,用它准没错。
FastDateFormat是Apache的commons-lang3包中提供的实现。满足线程安全,性能不差。如果项目还在广泛使用Date,那就选它吧。