轉(zhuǎn)載自 還在使用SimpleDateFormat瑞驱?你的項目崩沒?
還在使用SimpleDateFormat?你的項目崩沒拍顷?
論SimpleDateFormat線程安全問題及解決方案
日常開發(fā)中,我們經(jīng)常需要使用時間相關(guān)類督弓,說到時間相關(guān)類营曼,想必大家對SimpleDateFormat并不陌生。主要是用它進(jìn)行時間的格式化輸出和解析愚隧,挺方便快捷的蒂阱,但是SimpleDateFormat并不是一個線程安全的類。在多線程情況下狂塘,會出現(xiàn)異常录煤,想必有經(jīng)驗的小伙伴也遇到過。下面我們就來分析分析SimpleDateFormat為什么不安全荞胡?是怎么引發(fā)的妈踊?以及多線程下有那些SimpleDateFormat的解決方案?
先看看《阿里巴巴開發(fā)手冊》對于SimpleDateFormat是怎么看待的:
附《阿里巴巴Java開發(fā)手冊》v1.4.0(詳盡版)下載鏈接:https://yfzhou.oss-cn-beijing.aliyuncs.com/blog/img/《阿里巴巴開發(fā)手冊》v 1.4.0.pdf
問題場景復(fù)現(xiàn)
一般我們使用SimpleDateFormat的時候會把它定義為一個靜態(tài)變量泪漂,避免頻繁創(chuàng)建它的對象實例响委,如下代碼:
public class SimpleDateFormatTest {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date) throws ParseException {
return sdf.format(date);
}
public static Date parse(String strDate) throws ParseException {
return sdf.parse(strDate);
}
public static void main(String[] args) throws InterruptedException, ParseException {
System.out.println(sdf.format(new Date()));
}
}
是不是感覺沒什么毛病窖梁?單線程下自然沒毛病了赘风,都是運用到多線程下就有大問題了。
測試下:
public static void main(String[] args) throws InterruptedException, ParseException {
ExecutorService service = Executors.newFixedThreadPool(100);
for (int i = 0; i < 20; i++) {
service.execute(() -> {
for (int j = 0; j < 10; j++) {
try {
System.out.println(parse("2018-01-02 09:45:59"));
} catch (ParseException e) {
e.printStackTrace();
}
}
});
}
// 等待上述的線程執(zhí)行完
service.shutdown();
service.awaitTermination(1, TimeUnit.DAYS);
}
你看這不崩了纵刘?部分線程獲取的時間不對邀窃,部分線程直接報
java.lang.NumberFormatException: multiple points
錯,線程直接掛死了假哎。
多線程不安全原因
因為我們吧SimpleDateFormat定義為靜態(tài)變量瞬捕,那么多線程下SimpleDateFormat的實例就會被多個線程共享,B線程會讀取到A線程的時間舵抹,就會出現(xiàn)時間差異和其它各種問題肪虎。SimpleDateFormat和它繼承的DateFormat類也不是線程安全的
來看看SimpleDateFormat的format()方法的源碼
// 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;
}
注意calendar.setTime(date);
,SimpleDateFormat的format方法實際操作的就是Calendar惧蛹。
因為我們聲明SimpleDateFormat為static變量扇救,那么它的Calendar變量也就是一個共享變量,可以被多個線程訪問香嗓。
假設(shè)線程A執(zhí)行完calendar.setTime(date)迅腔,把時間設(shè)置成2019-01-02,這時候被掛起靠娱,線程B獲得CPU執(zhí)行權(quán)沧烈。線程B也執(zhí)行到了calendar.setTime(date),把時間設(shè)置為2019-01-03像云。線程掛起锌雀,線程A繼續(xù)走蚂夕,calendar還會被繼續(xù)使用(subFormat方法),而這時calendar用的是線程B設(shè)置的值了腋逆,而這就是引發(fā)問題的根源双抽,出現(xiàn)時間不對,線程掛死等等闲礼。
其實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.
意思就是
日期格式不同步牍汹。
建議為每個線程創(chuàng)建單獨的格式實例。
如果多個線程同時訪問一種格式柬泽,則必須在外部同步該格式慎菲。
解決方案
只在需要的時候創(chuàng)建新實例,不用static修飾
public static String formatDate(Date date) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.format(date);
}
public static Date parse(String strDate) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(strDate);
}
如上代碼锨并,僅在需要用到的地方創(chuàng)建一個新的實例露该,就沒有線程安全問題,不過也加重了創(chuàng)建對象的負(fù)擔(dān)第煮,會頻繁地創(chuàng)建和銷毀對象解幼,效率較低。
synchronized大法好
private static final 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);
}
}
簡單粗暴包警,synchronized往上一套也可以解決線程安全問題撵摆,缺點自然就是并發(fā)量大的時候會對性能有影響,線程阻塞害晦。
ThreadLocal
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);
}
ThreadLocal可以確保每個線程都可以得到單獨的一個SimpleDateFormat的對象特铝,那么自然也就不存在競爭問題了。
基于JDK1.8的DateTimeFormatter
也是《阿里巴巴開發(fā)手冊》給我們的解決方案壹瘟,對之前的代碼進(jìn)行改造:
public class SimpleDateFormatTest {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static String formatDate2(LocalDateTime date) {
return formatter.format(date);
}
public static LocalDateTime parse2(String dateNow) {
return LocalDateTime.parse(dateNow, formatter);
}
public static void main(String[] args) throws InterruptedException, ParseException {
ExecutorService service = Executors.newFixedThreadPool(100);
// 20個線程
for (int i = 0; i < 20; i++) {
service.execute(() -> {
for (int j = 0; j < 10; j++) {
try {
System.out.println(parse2(formatDate2(LocalDateTime.now())));
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
// 等待上述的線程執(zhí)行完
service.shutdown();
service.awaitTermination(1, TimeUnit.DAYS);
}
}
運行結(jié)果就不貼了鲫剿,不會出現(xiàn)報錯和時間不準(zhǔn)確的問題。
DateTimeFormatter源碼上作者也加注釋說明了稻轨,他的類是不可變的灵莲,并且是線程安全的。
* This class is immutable and thread-safe.
ok殴俱,現(xiàn)在是不是可以對你項目里的日期工具類進(jìn)行一波優(yōu)化了呢政冻?