Why is sharing a single SimpleDateFormat across threads dangerous, and what is the fix?
answer
- SimpleDateFormat mutable -> not thread-safe
- Shared internal Calendar corrupted by concurrent calls
- Symptoms: wrong dates, NumberFormatException, AIOOBE under load
- static final SimpleDateFormat = classic trap
- Fix: immutable DateTimeFormatter (or ThreadLocal/new-per-use)
basics
~10 sSimpleDateFormat is mutable and keeps internal state while formatting, so two threads using one instance corrupt each other, producing wrong or garbled dates. Fix it by using the immutable, thread-safe java.time DateTimeFormatter instead.
solid answer
~50 sSimpleDateFormat is part of the legacy date stack and is not thread-safe. While it formats or parses, it stores intermediate state in a shared internal Calendar field, so if two threads call format() or parse() on the same instance concurrently, they overwrite each other's state. The symptoms are nasty: occasional wrong dates, ParseExceptions, ArrayIndexOutOfBoundsException, or NumberFormatException under load — and it works fine in single-threaded tests, so the bug hides until production. People often make a SimpleDateFormat a static final constant for reuse, which is exactly the dangerous case. Safe options: create a new instance per use, confine one per thread with ThreadLocal, or synchronize access. The real fix is java.time.format.DateTimeFormatter, which is immutable and therefore inherently thread-safe — you declare one static final DateTimeFormatter and share it freely. Pair it with the immutable java.time types.
code
java · 14 lines// DANGEROUS: one shared mutable SimpleDateFormat across request threads
private static final SimpleDateFormat BAD = new SimpleDateFormat("yyyy-MM-dd");
// concurrent BAD.parse(...) -> wrong dates, NumberFormatException, AIOOBE
// SAFE: immutable DateTimeFormatter shared freely
private static final DateTimeFormatter GOOD =
DateTimeFormatter.ofPattern("yyyy-MM-dd");
String text = GOOD.format(LocalDate.now());
LocalDate parsed = LocalDate.parse("2026-06-20", GOOD);
// If you must keep SimpleDateFormat: confine per thread
private static final ThreadLocal<SimpleDateFormat> TL =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));go deeper
Knows SimpleDateFormat shouldn't be shared across threads and that DateTimeFormatter is the modern replacement.
Explains it is mutable/not thread-safe and can produce wrong results, and applies new-per-use or ThreadLocal as a fix.
Describes the shared internal Calendar mechanism and the production-only failure modes, and chooses immutable DateTimeFormatter as the correct fix.
Establishes formatter conventions across services, audits for the static-final SimpleDateFormat anti-pattern, and weighs ThreadLocal pool-leak vs allocation vs migration trade-offs.
## The setup `java.text.SimpleDateFormat` is the legacy class for turning a `java.util.Date` into a string (`format`) and a string back into a `Date` (`parse`), using a pattern like `"yyyy-MM-dd"`. It is the formatter half of the old Date/Calendar stack. ## What "not thread-safe" means here A class is **thread-safe** if multiple threads can use the same instance at once without producing wrong results or corruption. `SimpleDateFormat` is **mutable**: to do its job it holds an internal `Calendar` instance (inherited from `DateFormat`) and writes intermediate parsing/formatting state into it *during* a `format()` or `parse()` call. That internal state is shared across every caller of the instance. So if Thread A and Thread B call `format()` on the **same** `SimpleDateFormat` at the same time, Thread B can overwrite the Calendar fields Thread A is mid-way through using. There is no synchronization protecting it. ## The failure modes (why it's especially nasty) The corruption is non-deterministic and depends on timing, so symptoms vary: - A formatted date that is simply **wrong** (e.g. the year or month from another thread's value bleeds in). - `NumberFormatException` from `parse()` (the shared internal buffer got garbled). - `ArrayIndexOutOfBoundsException` deep inside the JDK's `DigitList`/`Calendar` code. - A `ParseException` on input that is actually valid. Crucially it **passes single-threaded unit tests** and only fails under concurrency — typically in a web server where one shared formatter handles many request threads. The classic anti-pattern is: ```java // DANGEROUS: shared mutable formatter private static final SimpleDateFormat FMT = new SimpleDateFormat("yyyy-MM-dd"); ``` Making it `static final` reads like good "reuse," but it maximizes the sharing that triggers the bug. ## Legacy-era fixes (if you're stuck on SimpleDateFormat) 1. **New instance per call** — simple and correct, but allocates on every use. 2. **`ThreadLocal<SimpleDateFormat>`** — one instance per thread, so no sharing; reused within a thread. Watch for leaks in thread pools (clean up if needed). 3. **`synchronized`** around every `format`/`parse` — correct but serializes access and becomes a contention bottleneck under load. ## The real fix: DateTimeFormatter `java.time.format.DateTimeFormatter` (Java 8+) is **immutable**. Because it never mutates internal state during formatting/parsing, it is **inherently thread-safe** with no synchronization. You declare it once and share it everywhere: ```java private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); String s = FMT.format(LocalDate.now()); // safe from any thread LocalDate d = LocalDate.parse("2026-06-20", FMT); // safe from any thread ``` It also offers predefined formatters (`DateTimeFormatter.ISO_LOCAL_DATE`), locale and zone support, and works directly with the immutable `java.time` types — so adopting it removes both the thread-safety problem and the rest of the legacy Date pitfalls at the same time. ## Takeaway The immutability of `DateTimeFormatter` is precisely why it is safe to make a `static final` constant, whereas the mutability of `SimpleDateFormat` is precisely why doing the same with it is a latent concurrency bug.
- Why is DateTimeFormatter safe to share when SimpleDateFormat is not?DateTimeFormatter is immutable — it holds no mutable per-operation state, so concurrent format/parse calls cannot interfere. SimpleDateFormat mutates a shared internal Calendar during each call, so concurrent use corrupts it.
- If you cannot migrate off SimpleDateFormat immediately, what is a reasonable interim fix?Use a ThreadLocal<SimpleDateFormat> so each thread has its own instance, or create a new instance per call. Synchronizing works but serializes access and hurts throughput under load.
saying these in an interview costs you the question
- Declaring a static final SimpleDateFormat as a shared constant
- Claiming SimpleDateFormat is fine because formatting 'looks read-only'
- Thinking the bug would surface in single-threaded tests
- Believing DateTimeFormatter needs synchronization