Java
SimpleDateFormat
Thread Safety
Synchronization
Concurrency

Synchronizing access to SimpleDateFormat

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

SimpleDateFormat is not thread-safe in Java. Sharing a single instance across threads without synchronization produces corrupted dates, wrong values, or NumberFormatException. The best fix in modern Java (8+) is to replace SimpleDateFormat with DateTimeFormatter, which is immutable and thread-safe. If you must use SimpleDateFormat, the options are: ThreadLocal<SimpleDateFormat>, explicit synchronized blocks, or creating a new instance per use. ThreadLocal gives the best performance; creating new instances is the simplest.

The Problem

java
1import java.text.SimpleDateFormat;
2import java.util.Date;
3
4// THIS IS BROKEN — shared mutable state across threads
5public class DateUtil {
6    // SimpleDateFormat is NOT thread-safe
7    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
8
9    public static String format(Date date) {
10        return sdf.format(date);  // Race condition: corrupted output
11    }
12
13    public static Date parse(String text) throws Exception {
14        return sdf.parse(text);   // Race condition: wrong dates or exceptions
15    }
16}

SimpleDateFormat uses internal mutable fields (calendar, numberFormat) during format() and parse(). When two threads call format() simultaneously, they overwrite each other's intermediate state, producing garbage output like "2025-03-2025" or throwing ArrayIndexOutOfBoundsException.

Solution 1: DateTimeFormatter (Best — Java 8+)

java
1import java.time.LocalDate;
2import java.time.LocalDateTime;
3import java.time.format.DateTimeFormatter;
4
5public class DateUtil {
6    // DateTimeFormatter is immutable and thread-safe — share freely
7    private static final DateTimeFormatter FORMATTER =
8        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
9
10    public static String format(LocalDateTime dateTime) {
11        return dateTime.format(FORMATTER);
12    }
13
14    public static LocalDateTime parse(String text) {
15        return LocalDateTime.parse(text, FORMATTER);
16    }
17
18    public static LocalDate parseDate(String text) {
19        return LocalDate.parse(text, DateTimeFormatter.ISO_LOCAL_DATE);
20    }
21}
22
23// Usage from any thread — no synchronization needed
24String formatted = DateUtil.format(LocalDateTime.now());
25LocalDateTime parsed = DateUtil.parse("2025-03-02 14:30:00");

Solution 2: ThreadLocal

java
1import java.text.SimpleDateFormat;
2import java.util.Date;
3
4public class DateUtil {
5    private static final ThreadLocal<SimpleDateFormat> SDF =
6        ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
7
8    public static String format(Date date) {
9        return SDF.get().format(date);  // Each thread gets its own instance
10    }
11
12    public static Date parse(String text) throws Exception {
13        return SDF.get().parse(text);
14    }
15}

Each thread gets its own SimpleDateFormat instance, eliminating contention. The instance is created lazily on first access and reused for subsequent calls on the same thread.

Solution 3: synchronized Block

java
1public class DateUtil {
2    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
3
4    public static synchronized String format(Date date) {
5        return sdf.format(date);
6    }
7
8    public static synchronized Date parse(String text) throws Exception {
9        return sdf.parse(text);
10    }
11}
12
13// Or with explicit lock for finer control
14public class DateUtil {
15    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
16    private static final Object lock = new Object();
17
18    public static String format(Date date) {
19        synchronized (lock) {
20            return sdf.format(date);
21        }
22    }
23}

Simple but creates a bottleneck — only one thread can format/parse at a time. Acceptable for low-contention scenarios.

Solution 4: New Instance Per Call

java
1public class DateUtil {
2    private static final String PATTERN = "yyyy-MM-dd";
3
4    public static String format(Date date) {
5        return new SimpleDateFormat(PATTERN).format(date);
6    }
7
8    public static Date parse(String text) throws Exception {
9        return new SimpleDateFormat(PATTERN).parse(text);
10    }
11}

The simplest approach but creates garbage for the GC on every call. Acceptable if formatting is infrequent.

Performance Comparison

java
1// Rough performance ranking (best to worst):
2// 1. DateTimeFormatter: immutable, no synchronization overhead, fastest
3// 2. ThreadLocal: one instance per thread, no contention, reusable
4// 3. New instance per call: GC pressure but no contention
5// 4. synchronized: single point of contention, blocks under load
6
7// For high-throughput applications:
8// - DateTimeFormatter: ~50ns per format
9// - ThreadLocal SDF: ~80ns per format
10// - New SDF per call: ~500ns per format (includes object creation)
11// - Synchronized SDF: depends on contention (unbounded under load)

Migrating from SimpleDateFormat to DateTimeFormatter

java
1import java.time.*;
2import java.time.format.DateTimeFormatter;
3import java.util.Date;
4
5public class DateMigration {
6    // Old: SimpleDateFormat
7    // new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
8
9    // New: DateTimeFormatter
10    private static final DateTimeFormatter FORMATTER =
11        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
12
13    // Convert Date to LocalDateTime
14    public static LocalDateTime fromDate(Date date) {
15        return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
16    }
17
18    // Convert LocalDateTime to Date
19    public static Date toDate(LocalDateTime ldt) {
20        return Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());
21    }
22
23    // Format with the new API
24    public static String format(Date date) {
25        return fromDate(date).format(FORMATTER);
26    }
27}

Common Pitfalls

  • Sharing a SimpleDateFormat instance as a static field: This is the root cause. Any static final SimpleDateFormat accessed from multiple threads is a data race. Either replace with DateTimeFormatter, wrap in ThreadLocal, or add synchronization. A single production incident from this bug can produce silently wrong dates in database records.
  • Using ThreadLocal in thread pools without cleanup: In application servers and thread pools, threads are reused. If ThreadLocal holds a SimpleDateFormat with a modified time zone or locale, subsequent requests on the same thread inherit stale settings. Call ThreadLocal.remove() in a finally block when the scope ends.
  • Forgetting that DateTimeFormatter.ofPattern is locale-sensitive: DateTimeFormatter.ofPattern("MMM dd") uses the JVM's default locale for month names. On a server with Locale.JAPAN, "Mar" becomes "3月". Use DateTimeFormatter.ofPattern("MMM dd", Locale.US) for consistent English output.
  • Mixing java.util.Date and java.time APIs carelessly: Converting between Date and LocalDateTime requires a timezone (ZoneId). Date.toInstant() gives UTC, but LocalDateTime has no timezone. Use ZonedDateTime or specify ZoneId.of("UTC") explicitly to avoid off-by-hours bugs.
  • Synchronized block scope too broad: Synchronizing an entire method that does I/O or computation beyond date formatting creates unnecessary contention. Synchronize only the format()/parse() call, not the surrounding logic.

Summary

  • SimpleDateFormat is not thread-safe — never share a single instance across threads without protection
  • Use DateTimeFormatter (Java 8+) as the default replacement — it is immutable and thread-safe
  • Use ThreadLocal<SimpleDateFormat> if you must stay on the old API for performance with thread safety
  • synchronized blocks work but create contention under high load
  • Migrate from java.util.Date + SimpleDateFormat to java.time + DateTimeFormatter for new code

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.