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.
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.
Full-Stack Internet Architecture
Introducing full-stack Internet architecture technologies centered on Java
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.