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

请用代码示例告诉为什么SimpleDateFormat不是线程安全的。 这个class有什么问题? 是与SimpleDateFormat的格式function的问题 ? 请给出一个在课堂上certificate这个错误的代码。

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

请解释说明这个问题的代码?

SimpleDateFormat在实例字段中存储中间结果。 所以如果一个实例被两个线程使用,他们可能会混淆对方的结果。

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

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

在不交易线程安全的情况下重用date格式的一种方法是把它们放在一个ThreadLocal – 一些库就是这样做的。 这就是如果你需要在一个线程中多次使用相同的格式。 但是如果你正在使用一个servlet容器(有一个线程池),记得在完成之后清理线程本地。

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

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

JavaDoc

但date格式不同步 。 build议为每个线程创build单独的格式实例。 如果多个线程同时访问一个格式, it must be synchronized externally

为了使SimpleDateFormat类是线程安全的,请看下面的方法 :

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

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

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

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

这是导致一个奇怪的错误的例子。 即使Google没有给出结果:

 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) 

这里是一个代码示例 ,certificate类中的错误。 我已经检查:使用parsing时,也是只使用格式时出现问题。

如果要在多个线程中使用相同的date格式,请在使用它时将其声明为静态并在实例variables上进行同步…

 static private SimpleDateFormat sdf = new SimpleDateFormat("...."); synchronized(sdf) { // use the instance here to format a date } // The above makes it thread safe 

以下是一个将SimpleDateFormat对象定义为静态字段的示例。 当两个或多个线程同时访问不同date的“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); } } 

您可以创build一个像下面这样的服务,并使用jmeter来模拟并发用户使用相同的SimpleDateFormat对象格式化不同的date,他们的结果将被搞乱。

 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脚本可以在这里下载。