在 Java 多線程程式設計中,DateFormat 及其子類 SimpleDateFormat 是常用的日期格式化工具。然而,許多開發者忽略了一個關鍵特性:DateFormat 不是線程安全的。官方 JavaDoc 明確指出:「Date formats 是不同步的。建議為每個線程創建獨立的日期格式實例。如果多線程並發訪問同一格式對象,必須在內部加上同步機制。」
以下程式碼展示了在單線程環境下正常運作的日期轉換類:
public class DateFormatTest {
private final DateFormat format = new SimpleDateFormat("yyyyMMdd");
public Date convert(String source) throws ParseException {
Date d = format.parse(source);
return d;
}
}
但當在多線程環境下測試時:
final DateFormatTest t = new DateFormatTest();
Callable<Date> task = new Callable<Date>() {
public Date call() throws Exception {
return t.convert("20100811");
}
};
ExecutorService exec = Executors.newFixedThreadPool(2);
List<Future<Date>> results = new ArrayList<>();
for (int i = 0; i < 5; i++) {
results.add(exec.submit(task));
}
exec.shutdown();
for (Future<Date> result : results) {
System.out.println(result.get());
}
執行結果極不穩定:有時輸出正確日期,有時出現錯誤日期(如 Sat Jul 31 00:00:00 BST 2012),甚至拋出 NumberFormatException。這是因為 SimpleDateFormat 內部使用共享的 Calendar 實例,多線程並發修改會導致狀態錯亂。
最直觀的方法是對格式化對象加鎖:
public Date convert(String source) throws ParseException {
synchronized (format) {
Date d = format.parse(source);
return d;
}
}
優點:實現簡單。
缺點:高併發下性能瓶頸明顯,線程需排隊等待。
為每個線程提供獨立的格式化實例:
public class DateFormatTest {
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyyMMdd");
}
};
public Date convert(String source) throws ParseException {
Date d = df.get().parse(source);
return d;
}
}
優點:無鎖競爭,性能優於同步方案。
缺點:線程池場景下需注意清理,防止內存洩漏。
Joda-Time 的 DateTimeFormatter 本身是線程安全且不可變的:
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import java.util.Date;
public class DateFormatTest {
private final DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyyMMdd");
public Date convert(String source) {
DateTime d = fmt.parseDateTime(source);
return d.toDate();
}
}
現代最佳實踐:Java 8+ 建議直接使用 java.time.format.DateTimeFormatter,它是不可變且線程安全的標準 API:
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
public LocalDate parse(String source) {
return LocalDate.parse(source, FORMATTER);
}
SimpleDateFormat 定義為 static 或實例變量供多線程共用java.time 包;舊版本可用 Joda-Time 或 ThreadLocalThreadLocal 適合高併發場景,同步鎖僅適用於低併發正確處理日期格式化的線程安全問題,是構建穩健 Java 多線程應用的基礎功課之一。