It comes as a surprise to many developers that SimpleDateFormat
instances are not thread-safe. Sometimes I encounter utility classes like below:
public class DateUtil { public static final SimpleDateFormat ISO_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); public static final SimpleDateFormat SQL_TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); // etc... public static Date parse(String date) throws ParseException { return ISO_DATE_FORMAT.parse(date); } public static String format(Date date) { return ISO_DATE_FORMAT.format(date); } // etc... } |
Looks innocent, but it will get you into trouble sooner or later if accessed by more than one thread at a time. And the problems are hard to find and debug, from strange dates in different application layers to strange exceptions where you’d expect none in a healthy system. And errors that only occurs very rare and more often when the system is under load…
Then what to do?
First thing, don’t ever do public static SimpleDateFormat
instances as you will have no control over what code will access the instance!
- Synchronize on the
SimpleDateFormat
instances? - Synchronize on parse/format utility methods?
- Always operate on new
SimpleDateFormat
instances?- Using new?
- Using clone()?
My latest solution to this is somewhat different, giving you the best of both worlds: no synchronization and only new instances for new threads. This is probably the best trade-off for situations where dates parsing and formatting is done with pooled threads as is often the case in application servers. If done with new short-lived threads only, there is little-to-no performance advantage compared to just creating new instances when needed.
The solution:
public class DateUtilThreadSafe { /** * Helper class for "thread-safe" SimpleDateFormat'ers, holding them in a ThreadLocal ensures * they are not called from different threads at the same time, without resorting to synchronization */ static class FormattersThreadCache extends ThreadLocal<Map<String, SimpleDateFormat>> { @Override protected Map<String, SimpleDateFormat> initialValue() { return new HashMap<String, SimpleDateFormat>(); } public SimpleDateFormat get(String formatString) { Map<String, SimpleDateFormat> map = get(); SimpleDateFormat sdf = map.get(formatString); if (sdf == null) { sdf = new SimpleDateFormat(formatString); map.put(formatString, sdf); } return sdf; } }; public static final String ISO_DATE_FORMAT = "yyyy-MM-dd"; public static final String SQL_TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss.SSS"; // extend to your needs... /** ThreadLocal with SimpleDateFormat'ers */ private static final FormattersThreadCache fmtCache = new FormattersThreadCache(); public static Date parse(String date) throws ParseException { return parse(ISO_DATE_FORMAT, date); } public static String format(Date date) { return format(ISO_DATE_FORMAT, date); } private static Date parse(String format, String date) throws ParseException { return fmtCache.get(format).parse(date); } private static String format(String format, Date date) { return fmtCache.get(format).format(date); } // extend to your needs... } |
Testing
Please consider attached test project. It contains some performance testing of different approaches and it contains a class that illustrates the types of error you encounter when accessing the same SimpleDateFormat
instance from different threads.
Try the ThreadingError
. It starts 2 threads that does parse()
and format()
on the same SimpleDateFormat
instance for 10 seconds. It registers first failure, either because of some internal error or because format()
or parse()
returns something unexpected.
Below are some samples of errors encountered during a couple of runs:
-------------------------------- java.lang.Exception: Date error. expected=Wed Jan 16 20:05:53 CET 2013, was=Fri Jan 16 20:05:53 CET 1 at net.udby.test.sdf.ThreadingError$1.run(ThreadingError.java:30) -------------------------------- java.lang.Exception: Date error. expected=Wed Jan 16 20:05:08 CET 2013, was=Sat Jan 16 20:05:08 CET 2213 at net.udby.test.sdf.ThreadingError$1.run(ThreadingError.java:30) -------------------------------- java.lang.Exception: Date error. expected=Wed Jan 16 20:02:09 CET 2013, was=Fri Jan 16 20:02:09 CET 2201 at net.udby.test.sdf.ThreadingError$1.run(ThreadingError.java:30) -------------------------------- java.lang.NumberFormatException: For input string: "" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Long.parseLong(Long.java:453) at java.lang.Long.parseLong(Long.java:483) at java.text.DigitList.getLong(DigitList.java:194) at java.text.DecimalFormat.parse(DecimalFormat.java:1316) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2086) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455) at java.text.DateFormat.parse(DateFormat.java:355) at net.udby.test.sdf.ThreadingError$1.run(ThreadingError.java:28) -------------------------------- java.lang.NumberFormatException: multiple points at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1101) at java.lang.Double.parseDouble(Double.java:540) at java.text.DigitList.getDouble(DigitList.java:168) at java.text.DecimalFormat.parse(DecimalFormat.java:1321) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1791) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455) at java.text.DateFormat.parse(DateFormat.java:355) at net.udby.test.sdf.ThreadingError$1.run(ThreadingError.java:28) -------------------------------- java.lang.NumberFormatException: For input string: "11.E111E" at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1241) at java.lang.Double.parseDouble(Double.java:540) at java.text.DigitList.getDouble(DigitList.java:168) at java.text.DecimalFormat.parse(DecimalFormat.java:1321) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1791) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455) at java.text.DateFormat.parse(DateFormat.java:355) at net.udby.test.sdf.ThreadingError$1.run(ThreadingError.java:28) -------------------------------- java.lang.NumberFormatException: For input string: "101.E1012E2" at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1241) at java.lang.Double.parseDouble(Double.java:540) at java.text.DigitList.getDouble(DigitList.java:168) at java.text.DecimalFormat.parse(DecimalFormat.java:1321) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1791) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455) at java.text.DateFormat.parse(DateFormat.java:355) at net.udby.test.sdf.ThreadingError$1.run(ThreadingError.java:28) -------------------------------- java.lang.ArrayIndexOutOfBoundsException: -1 at java.text.DigitList.fitsIntoLong(DigitList.java:229) at java.text.DecimalFormat.parse(DecimalFormat.java:1314) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2086) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455) at java.text.DateFormat.parse(DateFormat.java:355) at net.udby.test.sdf.ThreadingError$1.run(ThreadingError.java:28) -------------------------------- java.lang.NumberFormatException: For input string: "1616.E16162E2" at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1241) at java.lang.Double.parseDouble(Double.java:540) at java.text.DigitList.getDouble(DigitList.java:168) at java.text.DecimalFormat.parse(DecimalFormat.java:1321) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2086) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1455) at java.text.DateFormat.parse(DateFormat.java:355) at net.udby.test.sdf.ThreadingError$1.run(ThreadingError.java:28) |
Performance
Running the ReportDriver
tool generates a (csv) report with some timings.
This performance test tool tries 4 different approaches to parse()
and format()
:
DateFormatUtilClassSync
– uses (class-) synchronize on the static methods.DateFormatUtilCreate
– creates newSimpleDateFormat
instances using new.DateFormatUtilClone
– creates newSimpleDateFormat
instances by cloning existing instances.DateFormatUtilThreadLocal
– uses aThreadLocal
Map
to hold thread local instances.
It first warms up with a single thread for 10 seconds on each approach. Then it does “busy” testing where 4 threads are doing parse()
and format()
as fast as possible, using each approach for a minute.
The first conclusion is obvious (timings in µs):
Method | Avg |
---|---|
Synchronize | 31.2 |
Create – new | 17.9 |
Create – clone | 10.3 |
ThreadLocal | 6.7 |
Method | Avg |
---|---|
Synchronize | 36.1 |
Create – new | 30.4 |
Create – clone | 23.2 |
ThreadLocal | 18.0 |
The thread-local approach is by far the fastest, followed by create-clone, create-new and the synchronized the slowest. Watching performance on my dual-core laptop (with HT) while running this test shows about 49% utilization with the synchronized approach and 99..100% utilization with the other 3 approaches.
But, this test does almost nothing but parse and format. Is it realistic that a system does nothing but SimpleDateFormat
parse/format?
Not really… Most systems I’ve worked on do lots of stuff and then some parsing/formatting when interfacing with users etc.
Therefore there is also a second test, where each thread waits for 5ms between each busy parse/format. The figures are then quite different…
Method | Avg |
---|---|
Synchronize | 23.7 |
Create – new | 46.0 |
Create – clone | 27.6 |
ThreadLocal | 19.2 |
Method | Avg |
---|---|
Synchronize | 52.5 |
Create – new | 69.5 |
Create – clone | 53.9 |
ThreadLocal | 40.7 |
The thread-local approach is still fastest, but the synchronized follows closely. If you need to do dates formatting and parsing, synchronizing on instances or methods seems to be an acceptable approach, unless you have a busy system doing almost nothing else.
Link to report: ssdf.xls
Great example using date format with a strongly typed static thread local instance.
Commons Lang 3.x now has FastDateParser and FastDateFormat. These classes are thread safe and faster than SimpleDateFormat. They also support the same format/parse pattern specifications as SimpleDateFormat.
Thanks!
Pingback: SimpleDateFormat is NOT Thread-safe | Gan's Blog
Pingback: SimpleDateFormat – NumberFormatException: For input string: “” | Gan's Blog