为什么Java的SimpleDateFormat不是线程安全的?

266

请用一个代码示例说明SimpleDateFormat为什么不是线程安全的。这个类有什么问题? 问题出现在SimpleDateFormat的format函数中。 请给出一个演示该类错误的代码示例。

FastDateFormat是线程安全的。为什么? SimpleDateFormat和FastDateFormat之间有什么区别?

请提供一个演示此问题的代码示例。


2
FastDateFormat是commons-lang类的一部分:https://commons.apache.org/proper/commons-lang/javadocs/api-2.6/org/apache/commons/lang/time/FastDateFormat.html - Steve N
4
大多数开发人员都知道,对于大多数不是线程安全的类来说,这是由于同时更改状态造成的。一旦格式确定,格式化日期就不应该更改状态。将其在官方文档中简单地记录为不支持多线程不足够。如果格式方法在实例变量中保留临时状态,则应明确记录它不支持多线程。将其声明为静态方法并不只是新手错误。可以将修改集合(put)与访问集合(get)进行类比。 - YoYo
1
只是一个简短的真实故事:我已经运行了一款云应用程序约8年,几乎100%的正常运行时间。最近出现了一个与解析日期有关的奇怪个体错误。一个解析的日期是错误的。在代码审查期间,我发现SimpleDateFormat的使用方式不正确,这是一个线程安全问题。8年中只有一个错误!当然我会修复它。 - pcjuzer
我也犯了同样的错误,期望一旦设置格式和时区,formatparse方法就是线程安全的。目前我正在搜索并修复我们代码库中所有这些SimpleDateFormat的用法 :/ - Erich Kitzmueller
这个问题让我花了一些时间才找到,并且给客户带来了相当大的经济损失。简单来说,不要使用SimpleDateFormat,因为它不是线程安全的。请使用DateTimeFormatter。 - mujib ishola
1
不要使用DateFormatSimpleDateFormatDateCalendar类。这些可怕的类现在都已经过时了。它们早在多年前就被JSR 310中定义的现代java.time类所取代。java.time类是通过设计线程安全,使用不可变对象来实现的。 - Basil Bourque
9个回答

279

SimpleDateFormat将中间结果存储在实例字段中。因此,如果一个实例被两个线程使用,它们可能会干扰彼此的结果。

查看源代码可以发现有一个Calendar实例字段,用于操作DateFormat/SimpleDateFormat

例如,parse(..)最初调用calendar.clear(),然后调用calendar.add(..)。如果另一个线程在第一个调用完成之前调用parse(..),它将清除日历,但是其他调用将期望它填充计算的中间结果。

在不牺牲线程安全性的情况下重用日期格式的一种方法是将它们放入ThreadLocal中 - 一些库会这样做。如果您需要在一个线程中多次使用相同的格式,则可以这样做。但是,在使用Servlet容器(具有线程池)的情况下,请记得在完成后清除线程本地变量。

说实话,我不明白为什么他们需要实例字段,但事实就是这样。您也可以使用joda-timeDateTimeFormat,它是线程安全的。


72
他们不需要这个实例变量;它无疑是由于粗心编程和误导性的效率尝试而产生的结果。真正让人难以置信的是,这个陷阱早就应该被堵上了。我认为真正的解决方法是避免使用java.util.Date和Calendar。 - kevin cline
4
这个问题在JDK8中修复了吗?如果没有,那为什么? - dzieciou
34
在JDK8本身中,这个问题并没有被修复。但是JDK8引入了新的java.time包,其中包括线程安全的DateTimeFormatter。 - james turner
4
如果不破坏向后兼容性,它将永远无法“修复”。最好不要动它,让新代码使用更新的、线程安全的替代方案。 - whirlwin
3
@whirlwin 如果你不改变界面... - Enerccio
显示剩余6条评论

71

SimpleDateFormat 是一个具体类,用于以区域敏感的方式格式化和解析日期。

根据 JavaDoc 文件,

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

要使 SimpleDateFormat 类线程安全,请参考 以下方法

  • 每次需要使用 SimpleDateFormat 时都创建一个新实例。虽然这种方法是线程安全的,但它是最慢的方法。
  • 使用同步。这是一个不好的想法,因为你不应该在服务器上将线程崩溃。
  • 使用 ThreadLocal。这是三种方式中最快的方法 (请参见http://www.javacodegeeks.com/2010/07/java-best-practices-dateformat-in.html)。

9
这篇总结看起来不错,但我不同意作者的第二点。我怀疑同步日期格式不会成为服务器的瓶颈。这是否是 Knuth 所说的那 3% 必须进行过早优化的情况之一,还是属于那 97% 中“我们应该忘记小的低效率”?我曾见过一些人用定制的 web 框架,在 synchronized 块中包装控制器,并且所有访问都在其中,包括数据库调用、业务逻辑等,然后花费大量精力进行性能测试。难怪他们处于那 3% 中。 - michaelok
9
@michaelok 我必须同意!我认为恰恰相反-每当需要时创建新的日期格式化器,而不是使用一个单一的日期格式化器是过早的优化。您应该首先做简单的事情:每当需要时只需使用一个新实例。 - 仅当这成为性能问题(内存、GBC)时,您才应考虑共享实例-但请记住:您在线程之间共享的任何东西都可能成为等待着暗中竞争的潜在问题。 - Falco
4
顺便说一下,一个线程在日期格式化程序的例程中出现问题导致卡住了 - 突然间,当其他线程尝试访问日期格式化程序时,您的整个web服务器上的每个线程都会被卡住... 深思熟虑;-) - Falco
@michaelok 今天我们遇到了一个问题。 - Chad
2
你遇到的问题是什么 - 由于同步日期格式而遇到了瓶颈?还是线程安全的问题?正如James Turner上面提到的一件事,Java 8的Formatter确实是线程安全的:https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html - michaelok
显示剩余2条评论

68

1
是的,但您必须使用Temporal(LocalDate、LocalDateTime等)而不是SimpleDateFormat使用的java.util.Date。 - Saad Benbouzid
2
@SaadBenbouzid 请考虑这个优势。现代类比过时的 Date 类更易于使用,并提供更多可能性。 - Ole V.V.
1
是的,我有偏移量的问题。 - 3Qn

35

ThreadLocal + SimpleDateFormat = SimpleDateFormatThreadSafe

package com.foocoders.text;

import java.text.AttributedCharacterIterator;
import java.text.DateFormatSymbols;
import java.text.FieldPosition;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

public class SimpleDateFormatThreadSafe extends SimpleDateFormat {

    private static final long serialVersionUID = 5448371898056188202L;
    ThreadLocal<SimpleDateFormat> localSimpleDateFormat;

    public SimpleDateFormatThreadSafe() {
        super();
        localSimpleDateFormat = new ThreadLocal<SimpleDateFormat>() {
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat();
            }
        };
    }

    public SimpleDateFormatThreadSafe(final String pattern) {
        super(pattern);
        localSimpleDateFormat = new ThreadLocal<SimpleDateFormat>() {
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat(pattern);
            }
        };
    }

    public SimpleDateFormatThreadSafe(final String pattern, final DateFormatSymbols formatSymbols) {
        super(pattern, formatSymbols);
        localSimpleDateFormat = new ThreadLocal<SimpleDateFormat>() {
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat(pattern, formatSymbols);
            }
        };
    }

    public SimpleDateFormatThreadSafe(final String pattern, final Locale locale) {
        super(pattern, locale);
        localSimpleDateFormat = new ThreadLocal<SimpleDateFormat>() {
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat(pattern, locale);
            }
        };
    }

    public Object parseObject(String source) throws ParseException {
        return localSimpleDateFormat.get().parseObject(source);
    }

    public String toString() {
        return localSimpleDateFormat.get().toString();
    }

    public Date parse(String source) throws ParseException {
        return localSimpleDateFormat.get().parse(source);
    }

    public Object parseObject(String source, ParsePosition pos) {
        return localSimpleDateFormat.get().parseObject(source, pos);
    }

    public void setCalendar(Calendar newCalendar) {
        localSimpleDateFormat.get().setCalendar(newCalendar);
    }

    public Calendar getCalendar() {
        return localSimpleDateFormat.get().getCalendar();
    }

    public void setNumberFormat(NumberFormat newNumberFormat) {
        localSimpleDateFormat.get().setNumberFormat(newNumberFormat);
    }

    public NumberFormat getNumberFormat() {
        return localSimpleDateFormat.get().getNumberFormat();
    }

    public void setTimeZone(TimeZone zone) {
        localSimpleDateFormat.get().setTimeZone(zone);
    }

    public TimeZone getTimeZone() {
        return localSimpleDateFormat.get().getTimeZone();
    }

    public void setLenient(boolean lenient) {
        localSimpleDateFormat.get().setLenient(lenient);
    }

    public boolean isLenient() {
        return localSimpleDateFormat.get().isLenient();
    }

    public void set2DigitYearStart(Date startDate) {
        localSimpleDateFormat.get().set2DigitYearStart(startDate);
    }

    public Date get2DigitYearStart() {
        return localSimpleDateFormat.get().get2DigitYearStart();
    }

    public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition pos) {
        return localSimpleDateFormat.get().format(date, toAppendTo, pos);
    }

    public AttributedCharacterIterator formatToCharacterIterator(Object obj) {
        return localSimpleDateFormat.get().formatToCharacterIterator(obj);
    }

    public Date parse(String text, ParsePosition pos) {
        return localSimpleDateFormat.get().parse(text, pos);
    }

    public String toPattern() {
        return localSimpleDateFormat.get().toPattern();
    }

    public String toLocalizedPattern() {
        return localSimpleDateFormat.get().toLocalizedPattern();
    }

    public void applyPattern(String pattern) {
        localSimpleDateFormat.get().applyPattern(pattern);
    }

    public void applyLocalizedPattern(String pattern) {
        localSimpleDateFormat.get().applyLocalizedPattern(pattern);
    }

    public DateFormatSymbols getDateFormatSymbols() {
        return localSimpleDateFormat.get().getDateFormatSymbols();
    }

    public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) {
        localSimpleDateFormat.get().setDateFormatSymbols(newFormatSymbols);
    }

    public Object clone() {
        return localSimpleDateFormat.get().clone();
    }

    public int hashCode() {
        return localSimpleDateFormat.get().hashCode();
    }

    public boolean equals(Object obj) {
        return localSimpleDateFormat.get().equals(obj);
    }

}

https://gist.github.com/pablomoretti/9748230


6
我对于查找线程和同步所产生的开销是否比每次创建一个新实例的成本更大存在严重的疑虑。 - Jakub Bochenski
1
@JakubBochenski 这是一篇列出不同方法比较的文章。看起来 ThreadLocal 方法提供了最佳性能。https://www.javacodegeeks.com/2010/07/java-best-practices-dateformat-in.html - David Ruan
@DavidRuan谢谢,但是引用那篇文章的顶部评论:“能否提供源代码和测试代码?”。不知道是否经过适当的基准测试,这只是互联网上的随机图表。 - Jakub Bochenski
这个解决方案的问题在于它允许操作SimpleDateFormat,这可能会导致奇怪的状态!这是不一致和不线程安全的。如果SimpleDateFormat是不可变的,那么这个解决方案就会很聪明 - https://gist.github.com/pablomoretti/9748230#gistcomment-3758032 - CodingSamples

17

commons-lang的3.2版本将拥有FastDateParser类,它是公历下线程安全的SimpleDateFormat替代品。有关更多信息,请参见LANG-909


11

这里是一个导致奇怪错误的示例。即使在谷歌上搜索也没有任何结果:

public class ExampleClass {

private static final Pattern dateCreateP = Pattern.compile("Дата подачи:\\s*(.+)");
private static final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss dd.MM.yyyy");

public static void main(String[] args) {
    ExecutorService executor = Executors.newFixedThreadPool(100);
    while (true) {
        executor.submit(new Runnable() {
            @Override
            public void run() {
                workConcurrently();
            }
        });
    }
}

public static void workConcurrently() {
    Matcher matcher = dateCreateP.matcher("Дата подачи: 19:30:55 03.05.2015");
    Timestamp startAdvDate = null;
    try {
        if (matcher.find()) {
            String dateCreate = matcher.group(1);
            startAdvDate = new Timestamp(sdf.parse(dateCreate).getTime());
        }
    } catch (Throwable th) {
        th.printStackTrace();
    }
    System.out.print("OK ");
}
}

并且结果:

OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK OK java.lang.NumberFormatException: For input string: ".201519E.2015192E2"
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:2043)
at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
at java.lang.Double.parseDouble(Double.java:538)
at java.text.DigitList.getDouble(DigitList.java:169)
at java.text.DecimalFormat.parse(DecimalFormat.java:2056)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
at java.text.DateFormat.parse(DateFormat.java:364)
at com.nonscalper.webscraper.processor.av.ExampleClass.workConcurrently(ExampleClass.java:37)
at com.nonscalper.webscraper.processor.av.ExampleClass$1.run(ExampleClass.java:25)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)

请查看sgokhales在该线程中的答案。按照那些指南获取一个线程安全的SimpleDateFormat。 - sleepysilverdoor

6

以下是一个示例,将 SimpleDateFormat 对象定义为静态字段。当两个或更多线程同时访问“someMethod”,并使用不同的日期时,它们可能会干扰彼此的结果。

    public class SimpleDateFormatExample {
         private static final SimpleDateFormat simpleFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

         public String someMethod(Date date) {
            return simpleFormat.format(date);
         }
    }

您可以创建以下类似的服务,并使用jmeter模拟并发用户使用相同的SimpleDateFormat对象格式化不同的日期,其结果将会混乱。
public class FormattedTimeHandler extends AbstractHandler {

private static final String OUTPUT_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS";
private static final String INPUT_TIME_FORMAT = "yyyy-MM-ddHH:mm:ss";
private static final SimpleDateFormat simpleFormat = new SimpleDateFormat(OUTPUT_TIME_FORMAT);
// apache commons lang3 FastDateFormat is threadsafe
private static final FastDateFormat fastFormat = FastDateFormat.getInstance(OUTPUT_TIME_FORMAT);

public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
        throws IOException, ServletException {

    response.setContentType("text/html;charset=utf-8");
    response.setStatus(HttpServletResponse.SC_OK);
    baseRequest.setHandled(true);

    final String inputTime = request.getParameter("time");
    Date date = LocalDateTime.parse(inputTime, DateTimeFormat.forPattern(INPUT_TIME_FORMAT)).toDate();

    final String method = request.getParameter("method");
    if ("SimpleDateFormat".equalsIgnoreCase(method)) {
        // use SimpleDateFormat as a static constant field, not thread safe
        response.getWriter().println(simpleFormat.format(date));
    } else if ("FastDateFormat".equalsIgnoreCase(method)) {
        // use apache commons lang3 FastDateFormat, thread safe
        response.getWriter().println(fastFormat.format(date));
    } else {
        // create new SimpleDateFormat instance when formatting date, thread safe
        response.getWriter().println(new SimpleDateFormat(OUTPUT_TIME_FORMAT).format(date));
    }
}

public static void main(String[] args) throws Exception {
    // embedded jetty configuration, running on port 8090. change it as needed.
    Server server = new Server(8090);
    server.setHandler(new FormattedTimeHandler());

    server.start();
    server.join();
}

代码和jmeter脚本可以在这里下载。


3
这里有一个证明该类存在问题的代码示例。我已经检查过:当使用解析时,以及仅使用格式化时都会出现问题。

这段代码示例存在一些缺陷:由于并发问题,可能会抛出 NumberFormatException / ArrayIndexOutOfBoundsException 异常,并且它们会“悄悄地”终止线程。此外,线程没有被加入,这是不好的。请查看 LANG-909 中的类 - 我认为它们看起来更好。 - dma_k
1
@dma_k 我不太明白为什么你会在测试代码中加入线程,其唯一目的是失败和死亡。 :-) 无论如何:我并不想推荐博客中的ThreadSafeSimpleDateFormat(你是对的:有更好的解决方案),而是指出失败演示。 - Hans-Peter Störr
1
这对Unix测试更为重要,因为死亡线程不会影响测试本身的结果。是的,控制台上会打印一些内容,但从异常中无法识别是由于程序错误(格式/输入数据)还是并发问题引起的。代码本身很好,我的评论是针对那些将其复制/粘贴并在不同条件下使用的人。 - dma_k

-5
如果您想在多个线程中使用相同的日期格式,请将其声明为静态,并在使用时对实例变量进行同步...
static private SimpleDateFormat sdf = new SimpleDateFormat("....");

synchronized(sdf)
{
   // use the instance here to format a date
}


// The above makes it thread safe

1
但是获取sdf的监视器所浪费的时间肯定比每次创建一个新的监视器更多,不是吗? - saml
在Java中最慢的操作是调用new。 - Rodney P. Barbati
你将代码执行锁定在同步块中,只能使用单个线程管道。干得好(讽刺)。 - Marek Halmo

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