事实证明,Java 的 SimpleDateFormat 并没有那么简单。

格式化和解析日期是个(痛苦的)日常任务。每天,它都让我们很头疼。

在 Java 中格式化和解析日期的一种常见方法是使用 SimpleDateFormat。下面是我们用到的一个公共类。

 

import java.text.ParseException;	
import java.text.SimpleDateFormat;	
import java.util.Date;	

	
public final class DateUtils {	

	
    public static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("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) {	
        return SIMPLE_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 的内容:

日期格式是不同步的。

建议为每个线程创建独立的格式实例。

如果多个线程同时访问一个格式,则它必须是外部同步的。

Tip:当我们使用实例变量时,应始终检查其是否是一个线程安全类。

正如文档所述,我们为每个线程持有一个独立的变量来解决该问题。如果我们想共享对象?有什么解决方案?

方案一: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(() -> new SimpleDateFormat("yyyy-MM-dd"));	

	
    private DateUtilsThreadLocal() {}	

	
    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);	
    }	
}

方案二:Java 8 中线程安全的日期时间 API

Java 8 引入了一套新的日期时间 API。我们有一个更好的、麻烦更少的 SimpleDateFormat 替代品。如果我们真的需要坚持使用 SimpleDateFormat,可以继续使用 ThreadLocal。但是当有更好的选择时,我们应考虑使用它。

Java 8 引入了几个线程安全的日期类。

以下是 Java doc 的描述:

本类是不可变的,且线程安全的。

这些类是更加值得研究的,包括 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);	
    }	

	
}

3. 结论

Java 8 的解决方案使用不可变类,这是解决多线程问题的好方法。不可变类本质上是线程安全的,所以请尽可能地使用它们。

Happy coding!

 

 

 

 

不简单的 SimpleDateFormat_其他