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

Why is Java's SimpleDateFormat not thread-safe?

本问题已经有最佳答案,请猛点这里访问。

请告诉代码示例为什么SimpleDateFormat不是线程安全的。 这堂课有什么问题?
SimpleDateFormat的格式功能有问题吗?
请给出一个在课堂上演示此错误的代码。

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

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


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

查看源代码显示有一个Calendar实例字段,由DateFormat / SimpleDateFormat上的操作使用。

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

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

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


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

来自JavaDoc

But Date formats are not synchronized. It is recommended to create
separate format instances for each thread. If multiple threads access
a format concurrently, it must be synchronized externally.

要使SimpleDateFormat类是线程安全的,请查看以下方法:

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


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


ThreadLocal + SimpleDateFormat = SimpleDateFormatThreadSafe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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


commons-lang的版本3.2将具有FastDateParser类,它是格里高利历的SimpleDateFormat的线程安全替代。有关更多信息,请参阅LANG-909


这是导致奇怪错误的示例。即便谷歌也没有结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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");
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)


这是一个代码示例,证明了类中的错误。我已经检查过:使用解析时以及仅使用格式时会出现问题。


这是一个将SimpleDateFormat对象定义为静态字段的示例。当两个或多个线程同时访问不同日期的"someMethod"时,它们会混淆彼此的结果。

1
2
3
4
5
6
7
    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对象格式化不同的日期,他们的结果将被搞砸。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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脚本可以在这里下载。


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

1
2
3
4
5
6
7
8
9
static private SimpleDateFormat sdf = new SimpleDateFormat("....");

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


// The above makes it thread safe