Are NumberFormat and DecimalFormat thread-safe, and how should you use them in concurrent code?
answer
- NumberFormat/DecimalFormat NOT thread-safe (like SimpleDateFormat)
- mutable internal state (DigitList) corrupted by concurrent calls
- static final shared formatter = data race
- fixes: new per call / ThreadLocal / synchronized / immutable API
- DateTimeFormatter is the thread-safe date counterpart
basics
~20 sNo. NumberFormat and DecimalFormat are not thread-safe. If multiple threads share one instance and call format or parse at the same time, you can get wrong results or exceptions. Create a new instance per use, or give each thread its own via ThreadLocal.
solid answer
~50 sNumberFormat and DecimalFormat are explicitly documented as not thread-safe (the same is true of SimpleDateFormat). They keep mutable internal state during a format/parse call, so concurrent calls on one shared instance can interleave and corrupt that state, producing garbled output, wrong numbers, or NullPointerExceptions. The classic bug is a static final DecimalFormat field reused across request threads in a web app. Safe options: (1) create a fresh instance each time you format - cheap enough for most code; (2) use a ThreadLocal<DecimalFormat> so each thread holds its own instance; (3) synchronize access, though that serializes threads and hurts throughput; or (4) on Java 12+ use java.util.Formatter or, better, switch to immutable APIs. For new code, prefer formatting that doesn't share mutable state - or confine the formatter to a single thread. The key interview point: a shared static formatter is a latent data-race bug, not a harmless optimization.
code
java · 13 lines// UNSAFE: shared across threads -> intermittent wrong output / exceptions
private static final DecimalFormat SHARED = new DecimalFormat("#,##0.00");
// SAFE option A: a fresh instance each call
String a = new DecimalFormat("#,##0.00").format(value);
// SAFE option B: one per thread
private static final ThreadLocal<DecimalFormat> TL =
ThreadLocal.withInitial(() -> new DecimalFormat("#,##0.00"));
String b = TL.get().format(value);
// SAFE option C (rarely best): synchronize
synchronized (SHARED) { String c = SHARED.format(value); }go deeper
Knows you shouldn't share one formatter across threads and can create a new one each time.
Explains that the classes hold mutable state and names per-call or ThreadLocal fixes.
Diagnoses the static-shared-formatter data race, describes the symptoms, and chooses among per-call/ThreadLocal/synchronized with trade-offs.
Establishes a codebase convention (e.g. ban shared mutable formatters, prefer immutable APIs), and reasons about ThreadLocal retention in pooled threads and concurrency throughput impact.
## What 'thread-safe' means here Code is **thread-safe** if multiple threads can use it simultaneously without corrupting shared state or producing wrong results. An object is unsafe when a single method call temporarily mutates fields that another thread's concurrent call can clobber. `NumberFormat` and its subclass `DecimalFormat` fall in this category — and the Javadoc says so plainly: *number formats are generally not synchronized; create separate instances for each thread*. ## Why they are unsafe During a `format(...)` or `parse(...)` call, `DecimalFormat` writes to internal mutable helper objects (a shared `DigitList`, `FieldPosition` state, etc.). It does this to avoid reallocating on every call. If thread A is halfway through populating that digit buffer and thread B starts its own format on the **same instance**, B overwrites A's intermediate data. The observable symptoms are non-deterministic: occasionally swapped digits, a value from another thread's number, malformed strings, or a `NullPointerException` / `ArrayIndexOutOfBoundsException` deep in the formatter. Because it fails only under load and intermittently, it's a nasty production bug. ## The classic mistake ```java public class Money { // BUG: shared across all threads private static final DecimalFormat FMT = new DecimalFormat("#,##0.00"); public static String format(double v) { return FMT.format(v); } // data race } ``` This looks like a sensible optimization (build the formatter once) but is a latent race when called from a thread pool (e.g. a web server). ## Safe patterns 1. **New instance per call** — simplest and correct. Allocation is cheap relative to most work; profile before worrying. ```java public static String format(double v) { return new DecimalFormat("#,##0.00").format(v); } ``` 2. **ThreadLocal** — one instance per thread, reused within that thread. Good when formatting is hot. ```java private static final ThreadLocal<DecimalFormat> FMT = ThreadLocal.withInitial(() -> new DecimalFormat("#,##0.00")); // FMT.get().format(v) ``` Caveat: in a thread pool, remember each pooled thread keeps its copy for its lifetime; that's usually fine but is real retained memory. 3. **Synchronization** — wrap calls in `synchronized`. Correct but serializes threads, killing concurrency; rarely the best choice. 4. **Avoid mutable shared formatters** — prefer APIs without per-call mutable state. For dates, `java.time.format.DateTimeFormatter` is **immutable and thread-safe**; for numbers you can use `String.format`/`Formatter` (each call gets its own) or compute with `BigDecimal` and format locally. ## How to decide - Low call volume -> new instance per call. - High call volume, single-threaded confinement possible -> reuse locally. - High call volume across many threads -> ThreadLocal. - Never: one shared static/instance formatter touched by multiple threads without synchronization. ## Key takeaways 1. NumberFormat/DecimalFormat (and SimpleDateFormat) are **not thread-safe** — documented, not an accident. 2. The cause is mutable internal state reused per call. 3. A `static final` formatter shared across request threads is a data race. 4. Fix with per-call instances, ThreadLocal, synchronization, or immutable alternatives.
- What symptoms would a shared DecimalFormat race produce?Intermittent, load-dependent failures: occasionally garbled or swapped digits, a value belonging to another thread, malformed strings, or exceptions like NullPointerException/ArrayIndexOutOfBoundsException from inside the formatter.
- Is java.time.format.DateTimeFormatter also unsafe?No. DateTimeFormatter is immutable and explicitly thread-safe, which is one reason the java.time API is preferred over SimpleDateFormat for dates.
saying these in an interview costs you the question
- Treating a static final DecimalFormat as a safe shared singleton
- Assuming format()/parse() are read-only and therefore safe
- Reaching for synchronized first when ThreadLocal/per-call is cleaner
- Confusing it with the immutable, thread-safe DateTimeFormatter