SimpleDateFormat併發隱患及其解決-Joda-Time
SimpleDateFormat被大量使用於處理時間格式化過程,由於該類在建立時會指定一個pattern用於標明固定的時間格式,所以在使用中,一般會建立一個作用域較大(static修飾或某類的私有屬性)的物件用於重複使用。由於時間轉換過程遇到的多執行緒併發的使用場景並不多見,所以很難發現在該類的隱患,事實上,該類並非是執行緒安全的,在多執行緒使用format()和parse()方法時可能會遇到問題。
分析
在SimpleDateFormat及其父類DateFormat的原始檔裡,有這樣一段說明:
1234 |
* Date formats are not synchronized.* It is recommended to create separate format instances for |
JDK文件中已經明確指出,這兩個類在進行時間格式化的過程中都是非執行緒安全的。也就是說,使用同一個SimpleDateFormat例項,開若干執行緒做日期轉換操作,得到的結果可能並不準確。
parse
parse()測試,參考了其他人對此做的實驗,我使用的測試程式碼(jdk1.8)如下:
12345678910111213141516171819202122232425262728293031323334353637383940 |
import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class DateFormatTest extends Thread { private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); private |
這段測試程式碼參考了網上一段用例,與之不同的是,原用例中在兩個執行緒操作中間做了執行緒等待Sleep,而為了看到效果,修改後的測試用例把執行緒等待的部分去掉。雖然每次執行的結果都會不太一樣,但經常會丟擲的異常:
1234567891011121314 | Exception in thread "pool-1-thread-1" java.lang.NumberFormatException: For input string: "" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Long.parseLong(Long.java:601) at java.lang.Long.parseLong(Long.java:631) at java.text.DigitList.getLong(DigitList.java:195) at java.text.DecimalFormat.parse(DecimalFormat.java:2051) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514) at java.text.DateFormat.parse(DateFormat.java:364) at DateFormatTest.run(DateFormatTest.java:24) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745)Test_B : date: Mon Apr 28 00:00:00 CST 2200 |
需要明確的是,待轉換的字串作為非靜態私有變數是每個物件持有的,只有sdf本身是公用的,不難發現即便是成功輸出了,但是數值也未必會是正確的,parse()方法不安全。
format
SimpleDateFormat的format()方法原始碼如下:
12345 | private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) { // Convert input date to time field list calendar.setTime(date); ... |
需要注意的是calendar的操作並非是執行緒安全的,很顯然在併發情景下,format的使用並不安全,測試過程與對parse過程的測試相似,不再贅述。
解決
既然SimpleDateFormat本身並不安全,那麼解決的方式無非兩種:優化使用過程或者找替代品。
1.臨時建立
不使用Static,每次使用時,建立新例項。
存在的問題:
SimpleDateFormat中使用了Calendar物件,由於該物件相當重,在高併發的情況下會大量的new SimpleDateFormat以及銷燬SimpleDateFormat,極其耗費資源。
2.synchronized
以synchronized同步SimpleDateFormat物件。
存在的問題:
高併發時,使用該物件會出現阻塞,當前使用者使用時,其他使用者等待,儘管結果是對的,但是併發成了排隊,實際上並沒有解決問題,還會對效能以及效率造成影響。
3.ThreadLocal
使用ThreadLocal,令每個執行緒建立一個當前執行緒的SimpleDateFormat的例項物件。
public class ConcurrentDateUtil { private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static Date parse(String dateStr) throws ParseException { return threadLocal.get().parse(dateStr); } public static String format(Date date) { return threadLocal.get().format(date); } } 或 public class ThreadLocalDateUtil { private static final String date_format = "yyyy-MM-dd HH:mm:ss"; private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(); public static DateFormat getDateFormat() { DateFormat df = threadLocal.get(); if(df==null){ df = new SimpleDateFormat(date_format); threadLocal.set(df); } return df; } public static String formatDate(Date date) throws ParseException { return getDateFormat().format(date); } public static Date parse(String strDate) throws ParseException { return getDateFormat().parse(strDate); } }存在的問題:
使用ThreadLocal時,如果執行原子任務的過程是每一個執行緒執行一個任務,那麼這樣的宣告基本和每次使用前建立例項物件是沒區別的;如果使用的是多執行緒加任務佇列,舉個例子,tomcat有m個處理執行緒,外部有n個待處理任務請求,那麼當執行n個任務時,其實只會建立m個SimpleDateFormat例項,對於單一的處理執行緒,執行任務是有序的,所以對於當前執行緒而言,不存在併發。
4.Apache的 DateFormatUtils 與 FastDateFormat
使用org.apache.commons.lang.time.FastDateFormat 與 org.apache.commons.lang.time.DateFormatUtils。
存在的問題:
apache保證是執行緒安全的,並且更高效。但是DateFormatUtils與FastDateFormat這兩個類中只有format()方法,所有的format方法只接受long,Date,Calendar型別的輸入,轉換成時間串,目前不存在parse()方法,可由時間字串轉換為時間物件。
5.Joda-Time
使用Joda-Time類庫。
存在的問題:
沒有問題~
簡介:
Joda-Time — 面向 Java 應用程式的日期/時間庫的替代選擇,Joda-Time 令時間和日期值變得易於管理、操作和理解。事實上,易於使用是 Joda 的主要設計目標。其他目標包括可擴充套件性、完整的特性集以及對多種日曆系統的支援。並且 Joda 與 JDK 是百分之百可互操作的,因此您無需替換所有 Java 程式碼,只需要替換執行日期/時間計算的那部分程式碼。