SimpleDateFormat instances are not thread-safe

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 new SimpleDateFormat instances using new.
  • DateFormatUtilClone – creates new SimpleDateFormat instances by cloning existing instances.
  • DateFormatUtilThreadLocal – uses a ThreadLocal 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):

Format – busy
Method Avg
Synchronize 31.2
Create – new 17.9
Create – clone 10.3
ThreadLocal 6.7
Parse – busy
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…

Format – relaxed
Method Avg
Synchronize 23.7
Create – new 46.0
Create – clone 27.6
ThreadLocal 19.2
Parse – relaxed
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

About Jesper Udby

I'm a freelance computer Geek living in Denmark with my wife and 3 kids. I've done professional software development since 1994 and JAVA development since 1998.
This entry was posted in Java and tagged . Bookmark the permalink.

5 Responses to SimpleDateFormat instances are not thread-safe

  1. Pingback: SimpleDateFormat is NOT Thread-safe | Gan's Blog

  2. Pingback: SimpleDateFormat – NumberFormatException: For input string: “” | Gan's Blog

Leave a Reply to Chas Honton Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.