開發 Java 專案時經常操作時間、日期與字串的互相轉換,最常見簡單的方式是使用 SimpleDateFormat,想必大家對它不陌生。雖然它簡單易用,如果沒有正確使用,在一般環境下使用通常不會出錯,但在高併發(Highly Concurrent)的環境下就可能會出現異常。
我們都知道在程式中應盡量少使用 new SimpleDateFormat
,因為若頻繁實例化,則需要花費較多的成本,因此我們盡可能共用同一個實例。假設有一個轉換日期時間的 DateUtil
程式碼如下
public class DateUtil {
private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static String format(Date date) {
return simpleDateFormat.format(date);
}
}
不幸的是,共用 SimpleDateFormat
就是最典型的錯誤用法。官方文件提到:
Date formats are not synchronized. It is recommended to create separate format instances for each thread. If multiple threads access a format concurrently, it must be synchronized externally.
從 SimpleDateFormat 的原始碼中也可以看到它是有狀態的,而且其中 calendar 被宣告為成員變數,因此呼叫 format
, parse
等 method 時會多次存取此 calendar。在高併發環境下,將會造成 race condition,結果值就會不符預期,甚至拋出 exception。
幸運的是,已有許多解決方案:
public class DateUtil {
public static String format(Date date) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return simpleDateFormat.format(date);
}
}
這是最簡單的做法,只要每次都宣告區域變數就可以了,區域變數是 thread-safe。若專案對於效能要求不高,也許可以考慮這個解法,或直到出現效能問題時再考慮其他方法。畢竟至少這個做法能正確運作,而且簡單的作法往往是較好的。
public class DateUtil {
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
synchronized public static String format(Date date) {
return simpleDateFormat.format(date);
}
}
首先宣告 SimpleDateFormat field,避免重複 new 而造成效能問題。再加上關鍵字 synchronized
就能確保同一時刻只有一個 thread 能執行 format
(mutual exclusion)。雖然這個方式不會出錯,但可能降低併發度。
ThreadLocal 容器是一種讓程式達到 thread-safety 的手段,它相當於給每個 thread 都開了一個獨立的存儲空間,既然 thread 之間互相隔離,自然解決了 race condition 的問題,也讓 thread 能重複使用 SimpleDateFormat 實例。程式碼如下:
public class DateUtil {
// 可以把 ThreadLocal<SimpleDateFormat> 視為一個全域 Map<Thread, SimpleDateFormat>,key 就是 current thread
// 意義上相當於 currentThread 專屬、獨立的 cache。
private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<>();
private static SimpleDateFormat getDateFormat() {
// currentThread 從自己的 ThreadLocalMap 取得 SimpleDateFormat。
// 如果是 null,則建立 SimpleDateFormat 並放入自己的 ThreadLocalMap 中。
SimpleDateFormat dateFormat = local.get();
if (dateFormat == null) {
dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
local.set(dateFormat);
}
return dateFormat;
}
public static String format(Date date) {
return getDateFormat().format(date);
}
}
舉例來說,如果 thread pool 有 10 個 thread,程式就會建立 10 個 SimpleDateFormat 實例,這些 thread 們在每次的任務中重複使用各自的 SimpleDateFormat。但要注意一點,該 thread 能夠重複被使用(例如 server 在處理完一次 request 後,thread 會再回到 thread pool 待命),否則效果會和方法1差不多。這個方法的缺點是程式會變得較複雜。
雖然有點文不對題,畢竟這個問題困擾很多人許久了,因此在 Java 8 版本後官方就提供了 DateTimeFormatter
物件用來代替 SimpleDateFormat
。就像官方文件中說的:
DateTimeFormatter in Java 8 is immutable and thread-safe alternative to SimpleDateFormat.
簡單的範例如下:
將字串轉成 LocalDate
String dateStr = "2022/05/24";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
LocalDate date = LocalDate.parse(dateStr, formatter);
LocalDateTime 轉成字串
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm");
System.out.println(now.format(formatter));
本文轉錄自我的部落格 https://kaisheng714.github.io/articles/simple-date-format