Thread Safety of SimpleDateFormat and How to Solve It with ThreadLocal
This article explains why a static SimpleDateFormat instance is not thread‑safe, demonstrates the issue with concurrent parsing, and provides a ThreadLocal‑based solution with complete Java code examples and performance observations.
SimpleDateFormat internally holds a Calendar object, so a static instance shared by multiple threads can cause data races when methods like parse() clear and reuse the calendar.
The problem is illustrated by a multithreaded test where thread A sleeps, thread B hits a breakpoint, and both threads end up producing the same parsed date because they share the same Calendar state.
The simplest fix is to remove the static modifier, but that creates a new SimpleDateFormat for every request, which is costly under high concurrency.
Java’s ThreadLocal offers a better approach: each thread gets its own SimpleDateFormat instance, eliminating contention while reusing the formatter across multiple tasks executed by the same thread.
Below is a ThreadLocal‑based utility class that lazily creates a SimpleDateFormat per pattern per thread, stores them in a synchronized map, and provides format and parse methods:
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class DateUtil {
private static final Object lockObj = new Object();
private static Map
> sdfMap = new HashMap<>();
private static SimpleDateFormat getSdf(final String pattern) {
ThreadLocal
tl = sdfMap.get(pattern);
if (tl == null) {
synchronized (lockObj) {
tl = sdfMap.get(pattern);
if (tl == null) {
System.out.println("put new sdf of pattern " + pattern + " to map");
tl = new ThreadLocal
() {
@Override
protected SimpleDateFormat initialValue() {
System.out.println("thread: " + Thread.currentThread() + " init pattern: " + pattern);
return new SimpleDateFormat(pattern);
}
};
sdfMap.put(pattern, tl);
}
}
}
return tl.get();
}
public static String format(Date date, String pattern) {
return getSdf(pattern).format(date);
}
public static Date parse(String dateStr, String pattern) throws ParseException {
return getSdf(pattern).parse(dateStr);
}
}A test class creates several threads that call DateUtil.parse with different patterns, then runs them in single‑thread and two‑thread executor services to show that each thread reuses its own formatter without creating new instances for every task.
public class Test {
public static void main(String[] args) {
final String patten1 = "yyyy-MM-dd";
final String patten2 = "yyyy-MM";
// define six runnable threads that call DateUtil.parse with different inputs ...
// (code omitted for brevity) ...
System.out.println("单线程执行: ");
ExecutorService exec = Executors.newFixedThreadPool(1);
// submit threads t1‑t6 ...
exec.shutdown();
sleep(1000);
System.out.println("双线程执行: ");
ExecutorService exec2 = Executors.newFixedThreadPool(2);
// submit threads t1‑t6 ...
exec2.shutdown();
}
private static void sleep(long millSec) {
try { TimeUnit.MILLISECONDS.sleep(millSec); } catch (InterruptedException e) { e.printStackTrace(); }
}
}The console output confirms that with a single thread only one SimpleDateFormat per pattern is created, while with two threads each thread gets its own formatter, avoiding thread‑safety problems and reducing object creation overhead.
In summary, using ThreadLocal<SimpleDateFormat> is an efficient way to make date parsing/formatting thread‑safe in Java backend applications.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.