我们可以将SimpleDateFormat对象声明为静态对象吗?

23
SimpleDateFormat monthFormat = new SimpleDateFormat("MMMM");
SimpleDateFormat fullFormat = new SimpleDateFormat("EE MMM dd, HH:mm:ss")

我有几个经常被调用的代码块,将它们声明为static变量有意义吗?

在这种情况下,向format()方法传递动态参数是线程安全的吗?

6个回答

31

是线程安全的。请使用Joda-Time版本

或者将它们包装在同步方法中以使其线程安全。

文档清楚地说明了:

日期格式没有进行同步。建议为每个线程创建单独的格式实例。如果多个线程同时访问一个格式,则必须在外部进行同步。


4
在我看来,同步是一种非常次优的方式。关键部分应该始终是你最后的手段,并且仅用于必须既全局唯一又线程安全的对象。使用ThreadLocal。为每个新线程创建SimpleDateFormat的成本可靠地超过了同步的成本,无论是在性能还是复杂性上。 - pap
2
大多数开发人员都知道,对于大多数不是线程安全的类,这是由于同时更改状态。一旦格式确定,格式化日期就不应更改状态。仅在官方文档中记录此内容为非线程安全是不够的。如果格式方法在实例变量中维护临时状态,则应明确记录即使格式方法也不是线程安全的。将其声明为静态不仅是新手错误。可以将修改集合(put)与访问集合(get)进行类比。 - YoYo

26
自Java 8起,新的日期API支持此功能。 DateTimeFormatter是线程安全的,并且可以像SimpleDateFormat一样工作。引用自JavaDoc:

从模式创建的格式化程序可以使用多次,它是不可变的并且是线程安全的。

为了更加明确,定义格式如下也完全没问题:
private static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy MM dd");

并且在可以被多个线程同时访问的方法中使用它:

String text = date.toString(formatter);
LocalDate date = LocalDate.parse(text, formatter);

2

DateFormat不是线程安全的。如果多个线程在没有任何同步的情况下使用相同的DateFormat对象,则可能会得到意外的结果。因此,您应该同步访问DateFormat对象,使用ThreadLocal变量或使用另一种日期API,如Joda-Time。

有关如何实现此操作的更多信息,请参阅此博客文章:多线程下的DateFormat


2

0

SimpleDateFormat很容易使用,但也比较复杂。开发者通常编写代码并作为单个用户测试,并且会成功。但在并行和并发请求的情况下,SimpleDateFormat将是一个复杂的日期格式。

将其暴露为服务或控制器层中的静态变量时,当多个线程同时访问时,它可能会表现异常。

DateFormat API类还建议:“建议为每个线程创建单独的格式实例。如果多个线程并发访问格式,则必须在外部进行同步。”

演示多线程环境下失败的代码。

public final class DateUtils {

    public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

    private DateUtils() {
    }

    public static Date parse(String target) throws ParseException {
        return DATE_FORMAT.parse(target);
    }

    public static String format(Date target) {
        return DATE_FORMAT.format(target);
    }

    private static void testSimpleDateFormatWithThreads() {
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        final String source = "2019-01-11";

        IntStream.rangeClosed(0, 20)
                .forEach((i) -> executorService.submit(() -> {
                    try {
                        System.out.println(DateUtils.parse(source));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }));

        executorService.shutdown();
    }

    public static void main(String[] args) {
        testSimpleDateFormatWithThreads();
    }
}

输出 - 这取决于不同的机器,每次运行都会生成不同的输出。

2201年11月11日星期三IST 00:00:00

2019年1月11日星期五IST 00:00:00

2019年1月11日星期五IST 00:00:00

2019年1月11日星期五IST 00:00:00

2019年1月11日星期五IST 00:00:00

2018年12月31日星期一IST 00:00:00

2019年1月11日星期五IST 00:00:00

2019年1月11日星期五IST 00:00:00

2019年1月11日星期五IST 00:00:00

2019年1月11日星期五IST 00:00:00

2019年1月11日星期五IST 00:00:00

2019年1月11日星期五IST 00:00:00

2019年1月11日星期五IST 00:00:00

2019年1月11日IST 00:00:00

2019年1月11日IST 00:00:00

2019年1月11日IST 00:00:00

2019年1月11日IST 00:00:00

2201年1月11日IST 00:00:00

2019年1月11日IST 00:00:00

这是由于DateFormat->SimpleDateFormat类内的日历实例变量而导致的错误。

解决方案

  1. 使用ThreadLocal

    public static final ThreadLocal SIMPLE_DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

  2. Java 8,DateTimeFormatter API

    public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");


-2

static不应该是一个问题。

因为我知道没有关于线程安全的保证,所以你必须检查源代码。即使你得出结论认为它是线程安全的,这在下一个版本可能会改变。 如另一个答案中所述,它们不是线程安全的。

你真的会分配如此大量的线程,以至于给每个线程分配自己的格式是一个问题吗?


1
Javadoc 表明它不是线程安全的,因此将其设置为静态的是绝对不可以的。 - Kaj
这可能会导致业务应用程序中的严重错误,这些应用程序依赖于格式化日期输出。可能会混淆日期,完全或部分地。因此没有好的建议! - Andreas Krueger
@user326120 什么因素可能导致日期混乱? - Jens Schauder
@JensSchauder DateFormat.format的并发访问将返回混合日期,假设有DateFormat df = ...; 线程1使用df.format(d1)进入,然后线程2使用df.format(d2)进入,而线程1仍在执行df.format(d1)。- 由于DateFormat是可变的,否则它将是线程安全的,因此无法预测结果。(您可以编写一个小测试程序,同时创建10000个线程,通过一个单一的DateFormat实例输出格式化日期,而日期必须在日(月)和年份上有所不同,以便您可以告诉“混合”。探索它并告诉我们结果!) - Andreas Krueger
@Andreas Krueger 没有人说你应该同时访问它。 - Jens Schauder
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接