Back to ThreadLocal & Thread Confinement
Q2Question 2 of 6

Why is SimpleDateFormat not thread-safe?

Key Concepts
  • SimpleDateFormat is not thread-safe because format() and parse() write to a shared internal Calendar field.
  • Two threads sharing one SimpleDateFormat instance produce garbled dates, wrong dates, or NumberFormatException.
  • Fix 1: create a new SimpleDateFormat per call (safe but slow due to object creation overhead).
  • Fix 2: wrap in ThreadLocal so each thread has its own instance (safe and reusable, but call remove() in pools).
  • Fix 3: use DateTimeFormatter from java.time (Java 8+), which is immutable and thread-safe by design.
  • Always prefer java.time (LocalDate, LocalDateTime, DateTimeFormatter) in new code.

Definition

SimpleDateFormat is not thread-safe because it writes to a shared mutable Calendar field internally; fix it with ThreadLocal<SimpleDateFormat> or, better, use the immutable DateTimeFormatter from java.time.
  • SimpleDateFormat – not thread-safe because format() and parse() internally write to a mutable Calendar field shared across all calls.
  • Corruption symptoms – garbled dates, wrong dates, NumberFormatException, or ArrayIndexOutOfBoundsException when two threads use the same instance.
  • Fix 1: new instance per call – safe but creates garbage on every call, adding GC pressure at high throughput.
  • Fix 2: ThreadLocal – one instance per thread, reused within the thread, no sharing, no corruption.
  • Fix 3: DateTimeFormatter – immutable and thread-safe by design since Java 8; a single static final instance shared safely by all threads.
  • Best practice – always use java.time classes (LocalDate, LocalDateTime, DateTimeFormatter) in new code.

SimpleDateFormat is like a shared calculator on a desk at National Bank of India. Teller-1 types the date "25/10/2024" and the calculator shows an intermediate result. Teller-2 types "15/03/2025" simultaneously on the same calculator, and both results are garbled: "35/13/2025." ThreadLocal gives each teller their own calculator. DateTimeFormatter is a read-only printed conversion chart on the wall: 100 tellers read it simultaneously with no corruption because nobody writes on it.

Understanding the Concepts

Why is SimpleDateFormat not thread-safe?

SimpleDateFormat holds a mutable Calendar field internally. When format(Date) is called, it writes to this Calendar with calendar.setTime(date) and then reads from it with subFormat(). When two threads call format() on the same instance simultaneously, one thread's write overwrites the other's data, producing garbled output.

JAVA
// Internal implementation (simplified)
public String format(Date date) {
    calendar.setTime(date);   // Thread-1 writes here
    // Thread-2 overwrites calendar.setTime() at the same moment!
    return subFormat(...);    // reads corrupted Calendar state
}

How do you reproduce the bug?

JAVA
package com.bank.service;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.*;

public class DateFormatBug {
    // WRONG: shared mutable instance
    static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        Date date = new Date();

        for (int i = 0; i < 100; i++) {
            pool.submit(() -> {
                try {
                    String result = sdf.format(date);
                    System.out.println(result);
                } catch (Exception e) {
                    System.out.println("ERROR: " + e.getClass().getSimpleName());
                }
            });
        }
        pool.shutdown();
        pool.awaitTermination(5, TimeUnit.SECONDS);
    }
}
TEXT
2024-10-25
2024-10-25
ERROR: NumberFormatException
0025-10-25
2024-10-25

What are the three fixes?

Fix 1: new instance per call

JAVA
String formatted = new SimpleDateFormat("yyyy-MM-dd").format(date);
// Safe but creates a new object every call
// At 10,000 calls/second: significant GC pressure

Fix 2: ThreadLocal

JAVA
package com.bank.service;

import java.text.SimpleDateFormat;
import java.time.format.DateTimeFormatter;
import java.util.Date;

public class DateFormatFixes {

    // Fix 2: ThreadLocal, one instance per thread
    private static final ThreadLocal<SimpleDateFormat> dateFormatter =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

    public static String formatWithThreadLocal(Date date) {
        return dateFormatter.get().format(date);
        // Each thread has its own SimpleDateFormat
        // Remember: dateFormatter.remove() in finally in thread pools!
    }
}

Fix 3: DateTimeFormatter (best)

JAVA
package com.bank.service;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class DateFormatBest {

    // Fix 3: immutable, thread-safe, one instance for entire application
    private static final DateTimeFormatter FORMATTER =
        DateTimeFormatter.ofPattern("yyyy-MM-dd");

    public static String formatDate(LocalDate date) {
        return date.format(FORMATTER);
        // No ThreadLocal needed! No remove() needed!
        // Shared safely by all threads.
    }

    public static void main(String[] args) {
        System.out.println(formatDate(LocalDate.of(2024, 10, 25)));
    }
}
TEXT
2024-10-25

How does DateTimeFormatter compare to SimpleDateFormat?

| Feature | SimpleDateFormat | DateTimeFormatter |
|---|---|---|
| Thread-safe | No | Yes (immutable) |
| Mutable state | Calendar field | None |
| Sharing | Requires ThreadLocal | Share freely |
| API | Date, Calendar | LocalDate, LocalDateTime |
| Java version | 1.1+ | 8+ |
| Performance | Fast per-call | Fast per-call |
| Memory in pools | ThreadLocal overhead | Single static instance |

What are the java.time equivalents?

JAVA
// Old API                              New API (java.time)
SimpleDateFormat("yyyy-MM-dd")    →  DateTimeFormatter.ofPattern("yyyy-MM-dd")
sdf.format(new Date())            →  LocalDate.now().format(formatter)
sdf.parse("2024-10-18")           →  LocalDate.parse("2024-10-18", formatter)
Date                               →  LocalDate / LocalDateTime / ZonedDateTime
Calendar                           →  LocalDate / LocalDateTime (immutable)

What are common mistakes?

JAVA
// WRONG: static shared SimpleDateFormat
static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
// Multiple threads corrupt each other's Calendar

// WRONG: ThreadLocal without remove() in pool
static final ThreadLocal<SimpleDateFormat> tl =
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
// Memory leak in thread pool: remove() never called

// CORRECT: DateTimeFormatter, no cleanup needed
static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");

What are common interview follow-up questions?

  • Why is DateTimeFormatter thread-safe? It is immutable: no internal mutable state to corrupt.
  • Is FastDateFormat from Apache Commons thread-safe? Yes, it was created specifically to be a thread-safe alternative to SimpleDateFormat.
  • Can you synchronize on SimpleDateFormat instead? Yes, but it serializes all formatting calls and becomes a bottleneck under load.

Visual Overview

DIAGRAM
  SimpleDateFormat (NOT thread-safe)
  ┌──────────────────────────────────┐
  │  Internal mutable Calendar field  │
  │                                   │
  │  Thread-1: calendar.setTime(d1)  │
  │  Thread-2: calendar.setTime(d2)  │←── OVERWRITES!
  │  Thread-1: subFormat() reads d2  │←── WRONG DATA!
  └──────────────────────────────────┘

  DateTimeFormatter (thread-safe)
  ┌──────────────────────────────────┐
  │  Immutable: no internal state     │
  │                                   │
  │  Thread-1: format(d1) → OK       │
  │  Thread-2: format(d2) → OK       │
  │  No shared state to corrupt       │
  └──────────────────────────────────┘

Key Takeaway

SimpleDateFormat is one of the most common sources of concurrency bugs in Java applications. It holds a mutable Calendar field internally, and format() and parse() write to this field on every call. Two threads sharing one instance corrupt each other's data, producing garbled dates, wrong dates, or NumberFormatException.

Three fixes exist. Creating a new instance per call is safe but adds GC pressure at high throughput. ThreadLocal gives each thread its own instance with no sharing, but you must call remove() in thread pools to avoid memory leaks. DateTimeFormatter from java.time is the best solution: it is immutable and thread-safe by design, so a single static final instance can be shared by all threads with no ThreadLocal, no remove(), and no bugs.

At National Bank of India, a trade confirmation service with 200 threads used a static SimpleDateFormat. It garbled 50 confirmation dates per day for two months before anyone noticed. Replacing it with a static final DateTimeFormatter eliminated the bug completely with zero overhead.

Remember one sentence: SimpleDateFormat is mutable and broken under concurrency; use DateTimeFormatter, which is immutable and safe.

---

Share
Use ← → arrow keys to navigate