省流: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,那就选它吧。