Backend Development 13 min read

Understanding Thread Safety Issues with SimpleDateFormat and Solutions in Java

This article explains why SimpleDateFormat is not thread‑safe in Java, demonstrates the problem with multithreaded examples, and presents five practical solutions—including local variables, synchronized blocks, explicit locks, ThreadLocal, and the modern DateTimeFormatter—along with their advantages and drawbacks.

Full-Stack Internet Architecture
Full-Stack Internet Architecture
Full-Stack Internet Architecture
Understanding Thread Safety Issues with SimpleDateFormat and Solutions in Java

Thread safety, also known as non‑thread‑safe behavior, occurs when a multithreaded program produces results that differ from the expected outcome.

1. What Is Thread‑Unsafe?

SimpleDateFormat is a classic example of a thread‑unsafe class. The article first creates ten threads that format different timestamps using a shared SimpleDateFormat instance, showing that the printed results are inconsistent due to concurrent access.

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SimpleDateFormatExample {
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    Date date = new Date(finalI * 1000);
                    System.out.println(simpleDateFormat.format(date));
                }
            });
        }
    }
}

2. Solutions

The article lists five ways to solve the SimpleDateFormat thread‑unsafe problem:

Define SimpleDateFormat as a local variable – each thread creates its own instance, eliminating shared state.

Use synchronized to lock the formatter – serialize access to the shared SimpleDateFormat.

Use Lock (e.g., ReentrantLock) – similar to synchronized but with explicit lock control.

Use ThreadLocal – each thread holds its own formatter instance, avoiding lock contention.

Use JDK 8+ DateTimeFormatter – a thread‑safe alternative that works with LocalDateTime .

2.1 Local Variable

public class SimpleDateFormatExample {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
                    Date date = new Date(finalI * 1000);
                    System.out.println(simpleDateFormat.format(date));
                }
            });
        }
        threadPool.shutdown();
    }
}

2.2 Synchronized Lock

public class SimpleDateFormatExample2 {
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    Date date = new Date(finalI * 1000);
                    String result;
                    synchronized (simpleDateFormat) {
                        result = simpleDateFormat.format(date);
                    }
                    System.out.println(result);
                }
            });
        }
        threadPool.shutdown();
    }
}

2.3 Explicit Lock

public class SimpleDateFormatExample3 {
    private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("mm:ss");
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        Lock lock = new ReentrantLock();
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    Date date = new Date(finalI * 1000);
                    String result;
                    lock.lock();
                    try {
                        result = simpleDateFormat.format(date);
                    } finally {
                        lock.unlock();
                    }
                    System.out.println(result);
                }
            });
        }
        threadPool.shutdown();
    }
}

2.4 ThreadLocal

public class SimpleDateFormatExample4 {
    private static ThreadLocal
threadLocal =
        ThreadLocal.withInitial(() -> new SimpleDateFormat("mm:ss"));
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    Date date = new Date(finalI * 1000);
                    String result = threadLocal.get().format(date);
                    System.out.println(result);
                }
            });
        }
        threadPool.shutdown();
    }
}

2.5 DateTimeFormatter (JDK 8+)

public class SimpleDateFormatExample5 {
    private static DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("mm:ss");
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    Date date = new Date(finalI * 1000);
                    LocalDateTime localDateTime = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
                    String result = dateTimeFormatter.format(localDateTime);
                    System.out.println(result);
                }
            });
        }
        threadPool.shutdown();
    }
}

3. Why SimpleDateFormat Is Thread‑Unsafe

The source code of SimpleDateFormat.format shows that it uses a shared Calendar instance whose setTime method mutates internal state, causing race conditions when accessed concurrently.

4. Pros and Cons of Each Approach

If you are on JDK 8+, using the thread‑safe DateTimeFormatter is recommended. For older versions, synchronized is simple but may degrade performance, while ThreadLocal avoids lock contention. Defining the formatter as a local variable eliminates shared state but creates a new object per call, which may be less efficient.

Overall, choose the solution that balances safety, performance, and code maintainability for your specific Java version and application requirements.

JavaconcurrencyThread SafetysimpledateformatDateTimeFormatter
Full-Stack Internet Architecture
Written by

Full-Stack Internet Architecture

Introducing full-stack Internet architecture technologies centered on Java

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.