背景:
最近又看到亂用SimpleDateFormat的情況,這里做個關于SimpleDateFormat多線程下的安全性問題的總結.
之前部門集合了一個時間工具類供大家使用,里面各式各樣時間格式化的方法有幾十上百個樣子,然后由于很多方法都用的一個SimpleDateFormat,部門的機靈鬼發現這他娘不是重復代碼嘛?然后就把他提出來了,提出來后后面也沒發現什么問題,直到很久以后部門來了一個大流量的爬蟲任務需要并發處理task,然后頻繁調用時間格式化工具,然后在用這個SimpleDateFormat時候終于出現了問題,很多時間生成錯亂
,甚至根本不是一個時間的樣子,或者直接報錯了.
1.問題復現
1.1模擬并發使用SimpleDateFormat
public class TimeConcurrErrorTest {
static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
for (int i = 0; i <100 ; ++i) {
Thread thread = new Thread(()-> {
try {
System.out.println(sdf.parse("2020-12-11 11:17:27"));
} catch (ParseException e) {
e.printStackTrace();
}
});
thread.start();
}
}
}
1.2問題浮現
結果集如上圖所示,部分時間格式轉換沒有出現報錯,但是日期是千奇百怪的
,部分調用直接報錯了
1.3問題排查
protected Calendar calendar;
// Called from Format after creating a FieldDelegate
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;
}
SimpleDateFormat(下面簡稱sdf)類內部有一個Calendar對象引用,它用來儲存和這個sdf相關的日期信息,例如sdf.parse(dateStr), sdf.format(date) 諸如此類的方法參數傳入的日期相關String, Date等等, 都是交友Calendar引用來儲存的.這樣就會導致一個問題,如果你的sdf是個static的, 那么多個thread 之間就會共享這個sdf, 同時也是共享這個Calendar引用, 并且, 觀察 sdf.parse() 方法,你會發現有如下的調用:
Date parse() {
calendar.clear(); // 清理calendar
calendar.setTime(); // 設置calendar的時間
... // 執行一些操作
calendar.getTime(); // 獲取calendar的時間
}
這里會導致的問題就是, 如果 線程A 調用了 sdf.parse(), 并且進行了 calendar.clear()后還未執行calendar.getTime()的時候,線程B又調用了sdf.parse(), 這時候線程B也執行了sdf.clear()方法, 這樣就導致線程A的的calendar數據被清空了(實際上A,B的同時被清空了). 又或者當 A 執行了calendar.clear() 后被掛起, 這時候B 開始調用sdf.parse()并順利i結束, 這樣 A 的 calendar內存儲的的date 變成了后來B設置的calendar的date.亦或是并發setTime()等等問題.
這就造成了多線程并發修改的問題
2.問題解決
1.每次方法調用的時候都使用創建一個新的SimpleDateFormat自己用
缺點:如果我們同一線程多次調用格式化方法豈不是創建銷毀了很多次SimpleDateFormat?? 并發下一點點資源的損耗都會造成積少成多的情況,所以我們盡量減少重復資源的占用.這種方案可行但是不太好
2.對于單一線程頻繁使用SimpleDateFormat的,可以使用ThreadLocal存儲用時再取即可
3.使用java8提供的更安全的LocalDateTime (推薦!
)
核心思想:基于領域模型驅動設計方法以及不可變類,提供了各種各樣的安全類,比如做時間差的Duration,還有LocalDate,LocalTime,LocalDateTime等不可變類,并提供了相互的轉換方法
優點:
- 1.date有的LocalDateTime
都有
,有非常非常強大的Api,我也基于他的api封裝了一些工具類,但是公司代碼不好提供,大家可以直接參閱文檔 - 2.
安全可靠