Java SimpleDateFormat is NOT threadsafe

Elliott
3 min readJul 20, 2024

--

Please be careful if you use SimpleDateFormat to parse dates and share the class between multiple threads of execution. As noted in the Java doc:

Synchronization

Date formats are not synchronized. It is recommended to create a separate format instance for each thread. If multiple threads access the format concurrently, it should be synchronized externally.

API note:DateTimeFormatterConsider using as an immutable, thread-safe alternative.

Consider the following class:

package elliott.back;

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

public class SimpleDateFormatConcurrencyTest {
private static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
private static final TimeZone TIME_ZONE = TimeZone.getTimeZone("Asia/Tokyo");

static {
format.setTimeZone(TIME_ZONE);
}

public static Date startDate() {
Calendar calendar = Calendar.getInstance(TIME_ZONE);
calendar.set(1984, Calendar.JANUARY, 1, 0, 0, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}

public static Date endDate() {
Calendar calendar = Calendar.getInstance(TIME_ZONE);
calendar.set(1984, Calendar.DECEMBER, 31, 23, 59, 59);
calendar.set(Calendar.MILLISECOND, 999);
return calendar.getTime();
}

public static Date getRandomDate() {
// Get the time in milliseconds of the start and end dates
long startMillis = startDate().getTime();
long endMillis = endDate().getTime();

// Generate a random time in milliseconds within the range
long randomMillis = ThreadLocalRandom.current().nextLong(startMillis + 1000, endMillis - 1000);

// Create a new Date object with the random time
return new Date(randomMillis);
}

public static List<Date> parseDates(String[] dateStrings, int threadCount) throws InterruptedException, ExecutionException {
// Define a thread pool
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
List<Future<Date>> futureList = new ArrayList<>();

// Submit parsing tasks to the thread pool
for (String dateString : dateStrings) {
Callable<Date> task = () -> {
try {
return format.parse(dateString);
} catch (Exception e) {
return new Date(0);
}
};
Future<Date> future = executor.submit(task);
futureList.add(future);
}

// Collect the parsed dates
List<Date> parsedDates = new ArrayList<>();
for (Future<Date> future : futureList) {
parsedDates.add(future.get());
}

// Shut down the executor service
executor.shutdown();
return parsedDates;
}

public static double checkDates(List<Date> dates, Date start, Date end) {
long startl = start.getTime();
long endl = end.getTime();

Object[] outofBounds = dates.stream()
.filter(date -> date.getTime() < startl || date.getTime() > endl)
.toArray();

return (double) (dates.size() - outofBounds.length) / dates.size();
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
int count = 1000000; // a million random dates
String[] dates = new String[count];
for (int i = 0; i < count; i++)
dates[i] = format.format(getRandomDate());

for (int threads = 1; threads <= 1024; threads *= 2) {
List<Date> parsed = parseDates(dates, threads);
double percentage = checkDates(parsed, startDate(), endDate());
System.out.println(String.format("%d threads - %.2f%% inside bounds", threads, percentage * 100.0));
}
}
}

We run it (using JDK 21):

1 threads — 100.00% inside bounds
2 threads — 74.48% inside bounds
4 threads — 51.74% inside bounds
8 threads — 50.34% inside bounds
16 threads — 33.52% inside bounds
32 threads — 66.75% inside bounds
64 threads — 62.21% inside bounds
128 threads — 64.69% inside bounds
256 threads — 38.75% inside bounds
512 threads — 58.88% inside bounds
1024 threads — 59.50% inside bounds

I am not going to bother to measure this more qualitatively, but note that as many as 2/3 of your dates can be mangled by using the same SDF in a multithreaded context!

How do you fix this? Easy:

    private static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal =
ThreadLocal.withInitial(() -> {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
format.setTimeZone(TIME_ZONE);
return format;
});

Now everything works:

threadlocal: 1 threads — 100.00% inside bounds
threadlocal: 2 threads — 100.00% inside bounds
threadlocal: 4 threads — 100.00% inside bounds
threadlocal: 8 threads — 100.00% inside bounds
threadlocal: 16 threads — 100.00% inside bounds
threadlocal: 32 threads — 100.00% inside bounds
threadlocal: 64 threads — 100.00% inside bounds
threadlocal: 128 threads — 100.00% inside bounds
threadlocal: 256 threads — 100.00% inside bounds
threadlocal: 512 threads — 100.00% inside bounds
threadlocal: 1024 threads — 100.00% inside bounds

--

--

Elliott

Personal interests in literature, SF, and whisky/whiskey/scotch, Software Engineer by Trade