Java多線(xian)程:SimpleDateFormat
一、SimpleDateFormat的線程(cheng)安全問(wen)題
為什么SimpleDateFormat是線程不安全(quan)的?
public class DateUtilTest {
public static class TestSimpleDateFormatThreadSafe extends Thread {
@Override
public void run() {
while(true) {
try {
this.join(2000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
try {
System.out.println(this.getName()+":"+DateUtil.parse("2013-05-24 06:02:20"));
} catch (ParseException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
for(int i = 0; i < 3; i++){
new TestSimpleDateFormatThreadSafe().start();
}
}
}
執行輸出如下:
Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082)
at java.lang.Double.parseDouble(Double.java:510)
at java.text.DigitList.getDouble(DigitList.java:151)
at java.text.DecimalFormat.parse(DecimalFormat.java:1302)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
at java.text.DateFormat.parse(DateFormat.java:335)
at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17)
at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20)
Exception in thread "Thread-0" java.lang.NumberFormatException: multiple points
at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082)
at java.lang.Double.parseDouble(Double.java:510)
at java.text.DigitList.getDouble(DigitList.java:151)
at java.text.DecimalFormat.parse(DecimalFormat.java:1302)
at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
at java.text.DateFormat.parse(DateFormat.java:335)
at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17)
at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20)
Thread-2:Mon May 24 06:02:20 CST 2021
Thread-2:Fri May 24 06:02:20 CST 2013
說(shuo)明(ming):Thread-1和(he)Thread-0報java.lang.NumberFormatException: multiple points錯誤,直接掛(gua)(gua)死(si),沒起來;Thread-2 雖然沒有掛(gua)(gua)死(si),但(dan)輸(shu)出的(de)時間是有錯誤的(de),比如我們輸(shu)入的(de)時間是:2017-05-24 06:02:20 ,當會輸(shu)出:Mon May 24 06:02:20 CST 2021 這樣的(de)靈異(yi)事件
二.原因
作為一個專業(ye)程序(xu)員,我們當然都知道,相比(bi)于共享一個變量(liang)的(de)開銷要比(bi)每次創建一個新變量(liang)要小很(hen)多。上面(mian)的(de)優化過(guo)的(de)靜態的(de)SimpleDateFormat版,之所(suo)在并發情況下回出(chu)現各種靈異錯誤,是因為SimpleDateFormat和(he)DateFormat類不(bu)是線程安(an)全(quan)(quan)的(de)。我們之所(suo)以忽(hu)視(shi)線程安(an)全(quan)(quan)的(de)問(wen)題,是因為從SimpleDateFormat和(he)DateFormat類提供給我們的(de)接口上來看(kan),實在讓人看(kan)不(bu)出(chu)它與線程安(an)全(quan)(quan)有(you)何相干。只是在JDK文檔的(de)最下面(mian)有(you)如下說明:
SimpleDateFormat中的日期格式(shi)不(bu)是同步的。推薦(建(jian)議(yi))為每個(ge)線程(cheng)創建(jian)獨立的格式(shi)實例。如果多(duo)個(ge)線程(cheng)同時訪問(wen)一個(ge)格式(shi),則它(ta)必須(xu)保持外部同步。
JDK原始文檔如下:
Synchronization:
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.
下面我們通過看JDK源碼來(lai)看看為什么SimpleDateFormat和(he)DateFormat類不是線程安全的真(zhen)正原因:
SimpleDateFormat繼承(cheng)了DateFormat,在(zai)DateFormat中定義了一個protected屬(shu)性的(de) Calendar類(lei)的(de)對象:calendar。只是因為Calendar累的(de)概念復雜,牽扯到時(shi)(shi)區(qu)與本地化等等,Jdk的(de)實現(xian)(xian)中使用了成員變量來傳遞參數,這就造成在(zai)多(duo)線程的(de)時(shi)(shi)候會出現(xian)(xian)錯誤。
在(zai)format方(fang)法里,有這(zhe)樣(yang)一段代碼:
private StringBuffer format(Date date, StringBuffer toAppendTo,
FieldDelegate delegate) {
// Convert input date to time field list
calendar.setTime(date);
boolean useDateFormatSymbols = useDateFormatSymbols();
for (int i = 0; i < compiledPattern.length; ) {
int tag = compiledPattern[i] >>> 8;
int count = compiledPattern[i++] & 0xff;
if (count == 255) {
count = compiledPattern[i++] << 16;
count |= compiledPattern[i++];
}
switch (tag) {
case TAG_QUOTE_ASCII_CHAR:
toAppendTo.append((char)count);
break;
case TAG_QUOTE_CHARS:
toAppendTo.append(compiledPattern, i, count);
i += count;
break;
default:
subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);
break;
}
}
return toAppendTo;
}
calendar.setTime(date)這條(tiao)語句改變了(le)calendar,稍后,calendar還(huan)會用到(在(zai)subFormat方法(fa)里),而(er)這就(jiu)是引(yin)發問題(ti)的(de)根源。想象一下(xia),在(zai)一個多線(xian)程環境下(xia),有兩(liang)個線(xian)程持有了(le)同一個SimpleDateFormat的(de)實例,分別(bie)調(diao)用format方法(fa):
線程1調(diao)用(yong)format方法,改變了calendar這個字段。
中斷來了。
線程2開始執(zhi)行(xing),它也改變了calendar。
又中斷了。
線(xian)程(cheng)1回(hui)來了,此時,calendar已然不(bu)是它(ta)所設的值,而是走上了線(xian)程(cheng)2設計的道路。如果(guo)多個線(xian)程(cheng)同時爭搶calendar對象,則(ze)會出(chu)現各種問(wen)題,時間不(bu)對,線(xian)程(cheng)掛死等等。
分析(xi)一(yi)下(xia)format的實(shi)現,我們不難(nan)發(fa)現,用(yong)到成員變(bian)量calendar,唯一(yi)的好(hao)處,就是在調用(yong)subFormat時,少了(le)一(yi)個參數,卻帶來了(le)這(zhe)許多的問題。其(qi)實(shi),只要在這(zhe)里用(yong)一(yi)個局(ju)部變(bian)量,一(yi)路傳遞下(xia)去,所有問題都(dou)將迎刃而解。
這個(ge)問題背后(hou)隱(yin)藏著一個(ge)更(geng)為(wei)重要的問題--無狀(zhuang)態:無狀(zhuang)態方(fang)(fang)法(fa)(fa)(fa)的好(hao)處之一,就是(shi)它(ta)在各種環境下(xia),都可(ke)以安全的調用。衡(heng)量(liang)一個(ge)方(fang)(fang)法(fa)(fa)(fa)是(shi)否(fou)是(shi)有狀(zhuang)態的,就看它(ta)是(shi)否(fou)改動了其(qi)它(ta)的東西,比(bi)如全局變量(liang),比(bi)如實例的字段。format方(fang)(fang)法(fa)(fa)(fa)在運行過(guo)程中改動了SimpleDateFormat的calendar字段,所以,它(ta)是(shi)有狀(zhuang)態的。
這也同(tong)時提(ti)醒我們在開發和設計(ji)系統(tong)的時候注意(yi)下一下三(san)點:
1.自己(ji)寫(xie)公用類的時(shi)候,要對(dui)多線程調(diao)用情況(kuang)下的后(hou)果(guo)在注(zhu)釋里進(jin)行(xing)明確(que)說明
2.對線(xian)程環境下,對每一個(ge)共享(xiang)的可變(bian)(bian)變(bian)(bian)量都要注意其線(xian)程安全性
3.我們的類和方法(fa)在(zai)做設計(ji)的時候,要盡量設計(ji)成(cheng)無狀態(tai)的
三(san).解(jie)決辦法
1、使用同(tong)步
public class DateSyncUtil {
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);
}
}
}
說明:當線(xian)(xian)程較多(duo)時,當一(yi)個線(xian)(xian)程調用該(gai)方法(fa)時,其他想要調用此方法(fa)的(de)線(xian)(xian)程就(jiu)要block,多(duo)線(xian)(xian)程并(bing)發量大的(de)時候會(hui)對性能有(you)一(yi)定(ding)的(de)影響。
2、使用ThreadLocal
public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
public static final String PATTERN_DATE_TIME = "yyyy-MM-dd HH:mm:ss";
private static Map<String, ThreadLocal<DateFormat>> dateFormatMap;
/**
* 創建單例(li)
* 沒(mei)有使(shi)用(yong)(yong)單例(li)模式(shi)的話(hua),spring在注入數據時(shi)如果有邏輯調用(yong)(yong)了(le)getDateFormat(pattern),會因為(wei)dateFormatMap尚未初始化而(er)報NullPoint
* @return
*/
private static Map<String, ThreadLocal<DateFormat>> getDateFormatMap() {
if (dateFormatMap == null) {
synchronized (DateUtils.class) {
if (dateFormatMap == null) {
dateFormatMap = new ConcurrentHashMap<>();
}
}
}
return dateFormatMap;
}
/**
* 獲取線程安全的(de)DateFormat
* SimpleDateFormat有兩個問(wen)(wen)題(ti)(ti):
* 1.非線程安全,僅僅是聲明為static,使用時(shi)不(bu)上鎖在并(bing)發(fa)狀況下調用parse()有可(ke)(ke)能得(de)到(dao)錯誤(wu)的(de)時(shi)間
* 2.頻繁實例化(hua)有可(ke)(ke)能導致內存溢出
* 使用ThreadLocal將(jiang)DateFormat變(bian)為線程獨享,既可(ke)(ke)以避免并(bing)發(fa)問(wen)(wen)題(ti)(ti),又可(ke)(ke)以減少反(fan)復(fu)創(chuang)建實例的(de)開銷
* @param pattern
* @return
*/
public static DateFormat getDateFormat(final String pattern) {
ThreadLocal<DateFormat> dateFormat = getDateFormatMap().get(pattern);
if (dateFormat == null) {
dateFormat = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat(pattern);
}
};
dateFormatMap.put(pattern, dateFormat);
}
return dateFormat.get();
}
/**
* Date格式(shi)化 "yyyy-MM-dd HH:mm:ss"字符串
*
* @param date
* @return
*/
public static String DateFormatToString(Date date) {
DateFormat sdf = getDateFormat(PATTERN_DATE_TIME);
return sdf.format(date);
}
}
說明:使用(yong)ThreadLocal, 也是將共(gong)享變(bian)量變(bian)為獨(du)享,線(xian)程(cheng)獨(du)享肯(ken)定能(neng)比(bi)方法(fa)獨(du)享在并發環(huan)境中能(neng)減少(shao)不少(shao)創(chuang)建對(dui)象的開銷(xiao)。如(ru)果對(dui)性能(neng)要求比(bi)較(jiao)高的情況下(xia),一般(ban)推薦使用(yong)這(zhe)種方法(fa)。
3.拋(pao)棄JDK,使用其(qi)他類庫中的時間格式化類
1.使用(yong)Apache commons 里的(de)FastDateFormat,宣稱是(shi)既快又線程安全的(de)SimpleDateFormat, 可惜它只能對日(ri)期進(jin)(jin)行format, 不能對日(ri)期串進(jin)(jin)行解析。
2.使用Joda-Time類庫來處理時間相關問題
參考資料:
