Why are java.time types safe to share across threads and cache as constants, unlike java.util.Date and SimpleDateFormat?
answer
- Immutable + final fields → safe to share, no locks
- SimpleDateFormat keeps mutable state → NOT thread-safe
- DateTimeFormatter is immutable → share one static instance
- java.util.Date.setTime mutates; java.time has no setters
- Reference variable can still be reassigned (publish it properly)
basics
~10 sjava.time types never change after creation (immutable), so multiple threads reading the same instance can't interfere. The old Date is mutable and SimpleDateFormat keeps internal state, so sharing them across threads causes corruption.
solid answer
~40 sEvery java.time type is immutable and final, with no setters and only safely-published final fields, so once constructed an instance can be read by any number of threads without locks or copies — there is no state to corrupt. That makes them ideal as shared constants (e.g. a static EPOCH date) and as values passed between threads. The legacy API is the opposite: java.util.Date is mutable (setTime), and worse, SimpleDateFormat and Calendar keep mutable internal parsing/formatting state, so sharing a single SimpleDateFormat across threads produces silently wrong results or exceptions. The java.time replacement, DateTimeFormatter, is itself immutable and thread-safe, so one shared formatter instance is the recommended pattern. The practical senior takeaway: store java.time values and DateTimeFormatters as shared/static fields freely; never share a SimpleDateFormat — use a ThreadLocal or, better, migrate to DateTimeFormatter.
go deeper
Knows java.time objects are immutable and therefore safe to reuse; aware SimpleDateFormat is 'the old, unsafe one'.
Explains that immutability removes data races, and that DateTimeFormatter replaces the thread-unsafe SimpleDateFormat as a single shared instance.
Connects immutability + final fields + safe publication to lock-free sharing, diagnoses real SimpleDateFormat concurrency symptoms, and prescribes static-final formatter/value patterns.
Articulates the JMM final-field publication guarantee, distinguishes value-immutability from reference-reassignment visibility, and sets org-wide guidance for migrating legacy Date/Calendar/SimpleDateFormat code.
## The core property All `java.time` value types — `LocalDate`, `LocalTime`, `LocalDateTime`, `Instant`, `ZonedDateTime`, `OffsetDateTime`, `Duration`, `Period`, plus the formatter `DateTimeFormatter` — are **immutable** and declared **final**. Immutable means their fields are set once at construction and never change; final (class) means no subclass can add mutable state. There are no setters. This is a deliberate JSR-310 design goal. ## Why immutability gives thread safety for free A data race happens when two threads access shared mutable state and at least one writes, without coordination. If the state can **never be written after construction**, there is simply no write to race with. Provided the object is **safely published** (its reference made visible to other threads correctly — e.g. via a `final` field, a `static` field, a `volatile`, or a concurrent collection), every thread sees the fully-constructed, unchanging value. `java.time` types satisfy this because their internal fields are `final`, which the Java Memory Model guarantees are visible after construction. The result: **no locks, no synchronization, no defensive copies** needed to share them. ```java // Perfectly safe shared constant public static final LocalDate PROJECT_EPOCH = LocalDate.of(2020, 1, 1); // Perfectly safe shared formatter private static final DateTimeFormatter ISO = DateTimeFormatter.ISO_LOCAL_DATE; ``` ## The legacy contrast - **`java.util.Date` is mutable**: `setTime(long)` changes the instant in place. If you hand the same `Date` to two parts of a system, one can change it under the other. People worked around this with defensive copies; `java.time` removes the need entirely. - **`SimpleDateFormat` is mutable AND not thread-safe**: it stores intermediate parsing/formatting state (a `Calendar`, internal buffers) in fields during a `format`/`parse` call. Sharing one instance across threads causes interleaved calls to clobber that state — symptoms include wrong dates, `NumberFormatException`, `ArrayIndexOutOfBoundsException`, or values from another thread's request. This is a classic production bug. - **`Calendar` is also mutable** (`add`, `set` mutate in place), the very behavior that trains people to expect mutation and then trip over `java.time`'s discarded-result rule. ## The modern formatter `DateTimeFormatter` was redesigned to be **immutable and thread-safe**. You build it once (it is itself produced fluently/immutably via `withLocale`, `withZone`, etc., each returning a new formatter) and then share that single instance across all threads. So the correct pattern flips: with `SimpleDateFormat` you needed a `ThreadLocal<SimpleDateFormat>` (or a new instance per call); with `DateTimeFormatter` you want **one shared static instance**. ## Practical guidance 1. Store `java.time` values and `DateTimeFormatter`s as `static final` constants or shared fields without fear. 2. Pass them between threads/queues/futures with no copying. 3. Never share a `SimpleDateFormat`; if you must keep legacy code, wrap it in a `ThreadLocal` or, preferably, migrate to `DateTimeFormatter`. 4. Immutability is also why these types make good Map keys and equals/hashCode are stable — the hash can't change after insertion. ## The subtle caveat Immutability guarantees no *internal* mutation, but a **reference variable** can still be reassigned. `private LocalDate cursor` can be pointed at a new value by another thread; if many threads reassign a shared non-final field you still need normal publication (`volatile`/locking) for *that field*. The value itself is always safe; the variable holding it follows ordinary reference-visibility rules.
- What's the right replacement pattern for a shared SimpleDateFormat?Replace it with a single static final DateTimeFormatter, which is immutable and thread-safe; no ThreadLocal needed.
- Does immutability of LocalDate make a `private LocalDate cursor` field thread-safe to reassign?No. The value is safe, but reassigning the field is a write to shared state; you still need volatile/locking for the field itself.
- Why can SimpleDateFormat throw NumberFormatException under concurrency?Concurrent format/parse calls interleave on its shared internal Calendar/buffers, corrupting the parse state and producing bogus numbers or exceptions.
saying these in an interview costs you the question
- Claiming a shared SimpleDateFormat is fine if calls are 'quick'
- Wrapping DateTimeFormatter in a ThreadLocal (unnecessary)
- Saying immutability makes a reassignable field thread-safe too
- Defensively copying java.time values 'to be safe'