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:
DateTimeFormatter
Consider 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