为啥 Java 的 SimpleDateFormat 不是线程安全的? [复制]

Posted

技术标签:

【中文标题】为啥 Java 的 SimpleDateFormat 不是线程安全的? [复制]【英文标题】:Why is Java's SimpleDateFormat not thread-safe? [duplicate]为什么 Java 的 SimpleDateFormat 不是线程安全的? [复制] 【发布时间】:2011-10-14 00:35:03 【问题描述】:

请用代码示例说明为什么 SimpleDateFormat 不是线程安全的。这堂课有什么问题? SimpleDateFormat的格式化功能有问题? 请给出在课堂上演示此故障的代码。

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

请用演示此问题的代码解释一下?

【问题讨论】:

FastDateFormat 是一个 commons-lang 类:commons.apache.org/proper/commons-lang/javadocs/api-2.6/org/… 大多数开发人员都明白,对于大多数非线程安全的类,这是由于同时更改状态所致。建立格式后,格式化日期不应更改状态。仅在官方文档中将其记录为不是线程安全的是不够的。应该明确记录,即使格式方法在实例变量中保持临时状态,也不是线程安全的。将其声明为静态不仅仅是一个新手错误。可以在修改集合 (put) 与访问集合 (get) 之间进行类比。 只是一个简短的真实故事:我已经运行了一个基于云的应用程序大约 8 年,正常运行时间接近 100%。最近有一个与解析日期有关的奇怪的个别错误。一个解析的日期是错误的。在代码审查期间,我发现 SimpleDateFormat 使用错误,这是一个线程安全问题。一个错误8年!我当然会修复它。 我也犯了同样的错误,希望 formatparse 方法在设置格式和时区后是线程安全的。目前我正在我们的代码库中搜索和修复所有这些 SimpleDateFormat 用法:/ 这花了我一些时间来追踪,并让客户付出了很多钱。简单地说不要使用 SimpleDateFormat 它不是线程安全的使用 DateTimeFormatter 【参考方案1】:

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

查看source code 发现有一个Calendar 实例字段,供DateFormat / SimpleDateFormat 上的操作使用。

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

在不交易线程安全的情况下重用日期格式的一种方法是将它们放在ThreadLocal 中 - 一些库会这样做。那就是如果您需要在一个线程中多次使用相同的格式。但如果您使用的是 servlet 容器(具有线程池),请记住在完成后清理线程本地。

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

【讨论】:

他们不需要实例字段;毫无疑问,这是错误的效率尝试中草率编程的结果。真正令人难以置信的是,这个活板门很久以前就没有被钉上。我认为真正的答案是避免使用 java.util.Date 和 Calendar。 JDK8 中修复了这个问题吗?如果没有,那为什么不呢? JDK8 本身并没有修复这个问题。但是JDK8引入了新的java.time包,包括线程安全的DateTimeFormatter。 在不破坏向后兼容性的情况下,它永远无法“修复”。最好不要管它,让新代码只使用更新的、线程安全的替代方案。 . @whirlwin 如果你不改变界面...【参考方案2】:

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

来自JavaDoc

但日期格式不同步。建议创建 每个线程的单独格式实例。如果多个线程访问 一种格式,it must be synchronized externally

要使 SimpleDateFormat 类线程安全,请查看following approaches:

每次需要使用时创建一个新的 SimpleDateFormat 实例。虽然这是线程安全的,但它是最慢的方法。 使用同步。这是一个坏主意,因为您永远不应该在服务器上阻塞您的线程。 使用 ThreadLocal。这是 3 种方法中最快的方法(请参阅 http://www.javacodegeeks.com/2010/07/java-best-practices-dateformat-in.html)。

【讨论】:

这似乎是一个很好的总结,但我不同意作者的第二点。不知何故,我怀疑同步日期格式将成为您服务器上的瓶颈。在 Knuth 看来,这是需要过早优化的 3% 的情况之一,还是属于“我们应该忘记小的低效率”的 97%?现在,我已经看到人们使用自定义 Web 框架,将控制器包装在一个同步块中,因此除了数据库调用、业务逻辑之外的所有访问 - 然后在性能测试上花费了巨大的精力。难怪那里,他们在 3%。 @michaelok 我必须同意!我认为这只是另一种方式 - 使用一个 Single Dateformatter 而不是在需要时创建一个新的 Dateformatter 是过早的优化。你应该先做简单的事情:只要你需要一个新的实例就可以了。 - 只有当这成为性能问题(内存,GBC)时,您才应该考虑共享实例 - 但请记住:您在线程之间共享的任何内容都可能成为等待您的无声竞争条件。 顺便说一句。一个简单的点可能是一个线程因为任何问题而卡在 Dateformatter 的例程中 - 当他们尝试访问 DateFormatter 时,您的网络服务器上的每个和每个线程都会突然卡住... DED ;-) 您可以创建一个新实例或克隆它,这会更快一些。 @michaelok 今天我们遇到了问题。【参考方案3】:

Java 8 中的DateTimeFormatterSimpleDateFormat 的不可变且线程安全的替代方案。

【讨论】:

是的,但您必须使用 Temporal (LocalDate, LocalDateTime, ...) 而不是 SimpleDateFormat 使用的 java.util.Date @SaadBenbouzid 认为这是一个优势。现代类比过时的 Date 类更易于使用,并且提供了更多可能性。 是的,偏移量有问题。【参考方案4】:

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

【讨论】:

我非常怀疑线程查找和同步的开销是否不大于每次创建新实例的成本 @JakubBochenski 这是一篇列出不同方法比较的帖子。看起来 ThreadLocal 方法产生了最好的性能。 javacodegeeks.com/2010/07/… @DavidRuan 谢谢,但要引用那篇文章的最高评论:Could u please provide the source code and the testing code?。不知道它是否经过适当的基准测试,这只是互联网上的随机图表。 这个解决方案的问题是它允许操纵SimpleDateFormat,这可能会导致一个奇怪的状态!这是不一致的并且不是线程安全的。如果SimpleDateFormat 是不可变的,这个解决方案会很聪明-gist.github.com/pablomoretti/9748230#gistcomment-3758032【参考方案5】:

commons-lang 的 3.2 版将具有 FastDateParser 类,它是公历的 SimpleDateFormat 的线程安全替代品。请参阅LANG-909 了解更多信息。

【讨论】:

【参考方案6】:

这是导致奇怪错误的示例。甚至谷歌也没有给出任何结果:

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 的答案。遵循这些准则以获得线程安全的简单日期格式。【参考方案7】:

这是一个将 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脚本可以下载here.

【讨论】:

【参考方案8】:

这是一个code example,它证明了课堂上的错误。我已经检查过:使用 parse 以及仅使用 format 时会出现问题。

【讨论】:

此代码示例有一些缺陷:NumberFormatException / ArrayIndexOutOfBoundsException 也可能由于并发问题而被抛出,它们“默默地”杀死线程。线程也没有连接,这是不好的。查看LANG-909 中的课程——我认为它们看起来更好。 @dma_k 我不太明白为什么你会在测试代码中加入线程,其唯一目的是失败和死亡。 :-) 无论如何:我不想从博客中推荐 ThreadSafeSimpleDateFormat(你说得对:有更好的解决方案),而是指出失败的演示。 这对于 Unix 测试更为重要,因为死线程不会影响测试本身的结果。是的,有些东西会打印到控制台,但从异常中无法识别是由于程序错误(格式/输入数据)还是并发问题。代码本身就很好,我的评论是针对那些会复制/粘贴并在不同条件下使用的人。【参考方案9】:

如果要在多个线程之间使用相同的日期格式,请将其声明为静态并在使用时在实例变量上同步...

static private SimpleDateFormat sdf = new SimpleDateFormat("....");

synchronized(sdf)

   // use the instance here to format a date



// The above makes it thread safe

【讨论】:

但是,购买 sdf 的监视器所浪费的时间肯定比每次都创建一个新的还要多? 在 java 中可以执行的最慢的操作是调用 new。 + 你会将同步块上的代码执行踢到单线程管道上。干得好(讽刺)

以上是关于为啥 Java 的 SimpleDateFormat 不是线程安全的? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

SimpleDateForma求日期,2008-11月第6周星期日是几号?

javaweb 页面打印功能

java在框架左下角显示实时时间

java 时间现格式为00:00:00开始计时,如何表示

java 获取数据库中指定格式的日期

java怎么截取字符串?