Why Does SimpleDateFormat Fail in Multithreaded Java? Solutions and Best Practices
An operation team discovered that QR code redirects returned 404 due to expired access tokens, traced to SimpleDateFormat’s thread‑unsafe parsing causing incorrect expiration dates; the article analyzes the root cause, demonstrates failing multithreaded tests, and presents four thread‑safe alternatives including local instances, synchronization, ThreadLocal, and Java 8’s DateTimeFormatter.
1. Problem
Operations department reported that the QR code for a cash‑red‑envelope promotion configured in a mini‑program redirected to a 404 page after scanning.
2. Cause Investigation
First, the redirect link was not the actual URL of the QR code. The likely cause was that the accessToken had expired on the WeChat side. The database stored an accessToken expiration date of 2022‑11‑29, which was far beyond the investigation date (2022‑10‑08), so the token was not refreshed.
Next, logs showed that the SQL update statement used the same expiration time as the database record, suggesting a problem in the assignment code:
tokenInfo.setExpireTime(simpleDateFormat.parse(token.getString("expireTime")));Here, simpleDateFormat is a member variable.
Tracing the source revealed that SimpleDateFormat should not be used in multithreaded scenarios.
Synchronization
// SimpleDateFormat date formatting is not synchronized.
Date formats are not synchronized.
// It is recommended to create separate format instances for each thread.
// If multiple threads access a format concurrently, external synchronization is required.Thus, the incorrect expiration time stored in the database was caused by SimpleDateFormat.parse being used in a multithreaded context.
3. Cause Analysis
Write a test class to simulate the issue:
@RunWith(SpringRunner.class)
@SpringBootTest
public class SimpleDateFormatTest {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static final ExecutorService threadPool = new ThreadPoolExecutor(
16, 20, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(1024),
new ThreadFactoryBuilder().setNamePrefix("[Thread]").build(),
new ThreadPoolExecutor.AbortPolicy()
);
@SneakyThrows
@Test
public void testParse() {
Set<String> results = Collections.synchronizedSet(new HashSet<>());
String initialDateStr = "2022-10-08 18:30:01";
for (int i = 0; i < 20; i++) {
threadPool.execute(() -> {
Date parse = null;
try {
parse = simpleDateFormat.parse(initialDateStr);
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "---" + parse);
});
}
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);
}
}The execution produced multiple different dates and several exceptions, showing that SimpleDateFormat is not thread‑safe.
Why is SimpleDateFormat not thread‑safe?
SimpleDateFormat inherits from DateFormat, which holds a Calendar instance. The parse implementation modifies this shared Calendar, leading to race conditions when accessed by multiple threads.
@Override
public Date parse(String text, ParsePosition pos) {
// ... omitted ...
Date parsedDate;
try {
// ...
parsedDate = calb.establish(calendar).getTime();
} catch (IllegalArgumentException e) {
// ...
}
return parsedDate;
}The establish method clears and sets fields on the shared Calendar, causing overwrites and occasional clear‑induced errors in concurrent use.
4. Solution
1. Define SimpleDateFormat as a local variable and create a new instance for each use (recommended by JDK documentation, though it incurs object‑creation overhead).
public static Date parse(String strDate) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(strDate);
}2. Keep a static SimpleDateFormat instance and synchronize the formatting/parsing methods (not ideal for high‑concurrency systems).
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date) throws ParseException {
synchronized (sdf) {
return sdf.format(date);
}
}
public static Date parse(String strDate) throws ParseException {
synchronized (sdf) {
return sdf.parse(strDate);
}
}3. Use ThreadLocal to give each thread its own SimpleDateFormat instance, avoiding synchronization and reducing object‑creation cost.
public static ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
);
public static Date parse(String strDate) throws ParseException {
return threadLocal.get().parse(strDate);
}4. Switch to Java 8’s DateTimeFormatter, which is immutable and thread‑safe.
String dateTimeStr = "2016-10-25 12:00:00";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.parse(dateTimeStr, formatter);
System.out.println(localDateTime);
String format = localDateTime.format(formatter);
System.out.println(format);Finally, the project’s common DateUtil class implements the first approach (creating a new SimpleDateFormat for each conversion) to resolve the original issue.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Tuanzi Tech Team
Tuanzi Mobility, Ticketing & Cloud Systems – we provide mature industry solutions, share high‑quality technical insights, and warmly welcome everyone to follow and share.
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.
