Definition
- SimpleDateFormat – not thread-safe because
format()andparse()internally write to a mutableCalendarfield shared across all calls. - Corruption symptoms – garbled dates, wrong dates,
NumberFormatException, orArrayIndexOutOfBoundsExceptionwhen 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 finalinstance shared safely by all threads. - Best practice – always use
java.timeclasses (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.
// 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?
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);
}
}
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
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
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)
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)));
}
}
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?
// 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?
// 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
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.
---