“Java DateFormat is not threadsafe” what does this leads to?
每个人都警告Java DateFormat不是线程安全的,理论上我理解这个概念。
但是我无法想象出由此产生的实际问题。 比如,我在类中有一个DateFormat字段,并且在多线程环境中的类(格式化日期)中的不同方法中使用相同的字段。
这会导致:
- 格式异常等任何异常
- 数据差异
- 还有其他问题吗?
另外,请解释原因。
我们来试试吧。
这是一个程序,其中多个线程使用共享的
程序:
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 | public static void main(String[] args) throws Exception { final DateFormat format = new SimpleDateFormat("yyyyMMdd"); Callable<Date> task = new Callable<Date>(){ public Date call() throws Exception { return format.parse("20101022"); } }; //pool with 5 threads ExecutorService exec = Executors.newFixedThreadPool(5); List<Future<Date>> results = new ArrayList<Future<Date>>(); //perform 10 date conversions for(int i = 0 ; i < 10 ; i++){ results.add(exec.submit(task)); } exec.shutdown(); //look at the results for(Future<Date> result : results){ System.out.println(result.get()); } } |
运行几次,你会看到:
例外:
这里有一些例子:
1。
1 2 3 4 5 6 7 | Caused by: java.lang.NumberFormatException: For input string:"" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48) at java.lang.Long.parseLong(Long.java:431) at java.lang.Long.parseLong(Long.java:468) at java.text.DigitList.getLong(DigitList.java:177) at java.text.DecimalFormat.parse(DecimalFormat.java:1298) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589) |
2。
1 2 3 4 5 6 | Caused by: java.lang.NumberFormatException: For input string:".10201E.102014E4" at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1224) at java.lang.Double.parseDouble(Double.java:510) at java.text.DigitList.getDouble(DigitList.java:151) at java.text.DecimalFormat.parse(DecimalFormat.java:1303) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589) |
3。
1 2 3 4 5 6 7 | Caused by: java.lang.NumberFormatException: multiple points at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1084) at java.lang.Double.parseDouble(Double.java:510) at java.text.DigitList.getDouble(DigitList.java:151) at java.text.DecimalFormat.parse(DecimalFormat.java:1303) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1936) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1312) |
结果不正确:
1 2 3 4 5 6 7 8 9 10 | Sat Oct 22 00:00:00 BST 2011 Thu Jan 22 00:00:00 GMT 1970 Fri Oct 22 00:00:00 BST 2010 Fri Oct 22 00:00:00 BST 2010 Fri Oct 22 00:00:00 BST 2010 Thu Oct 22 00:00:00 GMT 1970 Fri Oct 22 00:00:00 BST 2010 Fri Oct 22 00:00:00 BST 2010 Fri Oct 22 00:00:00 BST 2010 Fri Oct 22 00:00:00 BST 2010 |
正确的结果:
1 2 3 4 5 6 7 8 9 10 | Fri Oct 22 00:00:00 BST 2010 Fri Oct 22 00:00:00 BST 2010 Fri Oct 22 00:00:00 BST 2010 Fri Oct 22 00:00:00 BST 2010 Fri Oct 22 00:00:00 BST 2010 Fri Oct 22 00:00:00 BST 2010 Fri Oct 22 00:00:00 BST 2010 Fri Oct 22 00:00:00 BST 2010 Fri Oct 22 00:00:00 BST 2010 Fri Oct 22 00:00:00 BST 2010 |
在多线程环境中安全使用DateFormats的另一种方法是使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public class DateFormatTest { private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){ @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyyMMdd"); } }; public Date convert(String source) throws ParseException{ Date d = df.get().parse(source); return d; } } |
这是一篇包含更多细节的好文章。
我希望数据损坏 - 例如如果你同时解析两个日期,你可能会有一个被另一个日期的数据污染的电话。
很容易想象这是如何发生的:解析通常涉及到目前为止你所阅读的内容保持一定的状态。如果两个线程都在相同的状态下践踏,那么你会遇到问题。例如,
我没有仔细研究
我个人会使用Joda Time的解析器,因为它们是线程安全的 - 而Joda Time是一个更好的日期和时间API开始:)
如果您使用的是Java 8,则可以使用
A formatter created from a pattern can be used as many times as
necessary, it is immutable and is thread-safe.
码:
1 2 3 4 |
输出:
1 | 2017-04-17 |
粗略地说,您不应将
Date formats are not synchronized. It is recommended to create separate format instances for each thread.
因此,如果您的
1 2 3 4 5 6 7 | public class Foo { private DateFormat df = new SimpleDateFormat("dd/mm/yyyy"); public void handleBar(Bar bar) { bar.setFormattedDate(df.format(bar.getStringDate()); } } |
你应该使用:
1 2 3 4 5 6 7 | public class Foo { public void handleBar(Bar bar) { DateFormat df = new SimpleDateFormat("dd/mm/yyyy"); bar.setFormattedDate(df.format(bar.getStringDate()); } } |
此外,在所有情况下,都没有
如Jon Skeet所述,如果执行外部同步(即使用
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.
这意味着假设你有一个DateFormat的对象,并且你正在从两个不同的线程访问同一个对象而你正在调用该对象的格式方法,两个线程将同时在同一个对象上输入同一个方法,这样你就可以想象它赢了结果不是很好
如果您必须使用DateFormat,那么您应该做些什么
1 2 3 | public synchronized myFormat(){ // call here actual format method } |
- 参考
在最好的答案中,dogbane给出了一个使用
请注意,如果更改执行程序(并发线程)的数量,您将得到不同的结果。从我的实验:
-
将
newFixedThreadPool 设置为5,每次循环都会失败。 - 设置为1并且循环将始终有效(显然所有任务实际上是逐个运行)
- 设置为2,循环只有大约6%的工作机会。
我猜YMMV取决于你的处理器。
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 | /** * Test SimpleDateFormat.format (non) thread-safety. * * @throws Exception */ private static void testFormatterSafety() throws Exception { final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); final Calendar calendar1 = new GregorianCalendar(2013,1,28,13,24,56); final Calendar calendar2 = new GregorianCalendar(2014,1,28,13,24,56); String expected[] = {"2013-02-28 13:24:56","2014-02-28 13:24:56"}; Callable<String> task1 = new Callable<String>() { @Override public String call() throws Exception { return"0#" + format.format(calendar1.getTime()); } }; Callable<String> task2 = new Callable<String>() { @Override public String call() throws Exception { return"1#" + format.format(calendar2.getTime()); } }; //pool with X threads // note that using more then CPU-threads will not give you a performance boost ExecutorService exec = Executors.newFixedThreadPool(5); List<Future<String>> results = new ArrayList<>(); //perform some date conversions for (int i = 0; i < 1000; i++) { results.add(exec.submit(task1)); results.add(exec.submit(task2)); } exec.shutdown(); //look at the results for (Future<String> result : results) { String answer = result.get(); String[] split = answer.split("#"); Integer calendarNo = Integer.parseInt(split[0]); String formatted = split[1]; if (!expected[calendarNo].equals(formatted)) { System.out.println("formatted:" + formatted); System.out.println("expected:" + expected[calendarNo]); System.out.println("answer:" + answer); throw new Exception("formatted != expected"); /** } else { System.out.println("OK answer:" + answer); /**/ } } System.out.println("OK: Loop finished"); } |
Format,NumberFormat,DateFormat,MessageFormat等的规范并非设计为线程安全的。此外,parse方法调用
更多,这些是错误报告,如this和this,以及DateFormat线程安全问题的结果。
数据已损坏。昨天我注意到它在我的多线程程序中,我有静态
其他答案向您展示了避免此类腐败的方法。我使
这是我的简单代码,显示DateFormat不是线程安全的。
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 | import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; public class DateTimeChecker { static DateFormat df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH); public static void main(String args[]){ String target1 ="Thu Sep 28 20:29:30 JST 2000"; String target2 ="Thu Sep 28 20:29:30 JST 2001"; String target3 ="Thu Sep 28 20:29:30 JST 2002"; runThread(target1); runThread(target2); runThread(target3); } public static void runThread(String target){ Runnable myRunnable = new Runnable(){ public void run(){ Date result = null; try { result = df.parse(target); } catch (ParseException e) { e.printStackTrace(); System.out.println("Ecxfrt"); } System.out.println(Thread.currentThread().getName() +" " + result); } }; Thread thread = new Thread(myRunnable); thread.start(); } } |
由于所有线程都使用相同的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 | Exception in thread"Thread-0" Exception in thread"Thread-2" Exception in thread"Thread-1" java.lang.NumberFormatException: multiple points at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source) at sun.misc.FloatingDecimal.parseDouble(Unknown Source) at java.lang.Double.parseDouble(Unknown Source) at java.text.DigitList.getDouble(Unknown Source) at java.text.DecimalFormat.parse(Unknown Source) at java.text.SimpleDateFormat.subParse(Unknown Source) at java.text.SimpleDateFormat.parse(Unknown Source) at java.text.DateFormat.parse(Unknown Source) at DateTimeChecker$1.run(DateTimeChecker.java:24) at java.lang.Thread.run(Unknown Source) java.lang.NumberFormatException: multiple points at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source) at sun.misc.FloatingDecimal.parseDouble(Unknown Source) at java.lang.Double.parseDouble(Unknown Source) at java.text.DigitList.getDouble(Unknown Source) at java.text.DecimalFormat.parse(Unknown Source) at java.text.SimpleDateFormat.subParse(Unknown Source) at java.text.SimpleDateFormat.parse(Unknown Source) at java.text.DateFormat.parse(Unknown Source) at DateTimeChecker$1.run(DateTimeChecker.java:24) at java.lang.Thread.run(Unknown Source) java.lang.NumberFormatException: multiple points at sun.misc.FloatingDecimal.readJavaFormatString(Unknown Source) at sun.misc.FloatingDecimal.parseDouble(Unknown Source) at java.lang.Double.parseDouble(Unknown Source) at java.text.DigitList.getDouble(Unknown Source) at java.text.DecimalFormat.parse(Unknown Source) at java.text.SimpleDateFormat.subParse(Unknown Source) at java.text.SimpleDateFormat.parse(Unknown Source) at java.text.DateFormat.parse(Unknown Source) at DateTimeChecker$1.run(DateTimeChecker.java:24) at java.lang.Thread.run(Unknown Source) |
但是如果我们将不同的对象传递给不同的线程,代码就会运行
没有错误。
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 | import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; public class DateTimeChecker { static DateFormat df; public static void main(String args[]){ String target1 ="Thu Sep 28 20:29:30 JST 2000"; String target2 ="Thu Sep 28 20:29:30 JST 2001"; String target3 ="Thu Sep 28 20:29:30 JST 2002"; df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH); runThread(target1, df); df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH); runThread(target2, df); df = new SimpleDateFormat("EEE MMM dd kk:mm:ss z yyyy", Locale.ENGLISH); runThread(target3, df); } public static void runThread(String target, DateFormat df){ Runnable myRunnable = new Runnable(){ public void run(){ Date result = null; try { result = df.parse(target); } catch (ParseException e) { e.printStackTrace(); System.out.println("Ecxfrt"); } System.out.println(Thread.currentThread().getName() +" " + result); } }; Thread thread = new Thread(myRunnable); thread.start(); } } |
这些是结果。
1 2 3 | Thread-0 Thu Sep 28 17:29:30 IST 2000 Thread-2 Sat Sep 28 17:29:30 IST 2002 Thread-1 Fri Sep 28 17:29:30 IST 2001 |
如果有多个线程操纵/访问单个DateFormat实例并且未使用同步,则可能会得到加扰结果。那是因为多个非原子操作可能会改变状态或看到内存不一致。