還在使用 SimpleDateFormat ?你的項目崩沒? [復制鏈接]

2019-5-14 09:39
一碼到底 閱讀:438 評論:1 贊:0
Tag:  

日常開發中,我們經常需要使用時間相關類,說到時間相關類,想必大家對SimpleDateFormat并不陌生。主要是用它進行時間的格式化輸出和解析,挺方便快捷的,但是SimpleDateFormat并不是一個線程安全的類。在多線程情況下,會出現異常,想必有經驗的小伙伴也遇到過。下面我們就來分析分析SimpleDateFormat為什么不安全?是怎么引發的?以及多線程下有那些SimpleDateFormat的解決方案?


先看看《阿里巴巴開發手冊》對于SimpleDateFormat是怎么看待的:



問題場景復現


一般我們使用SimpleDateFormat的時候會把它定義為一個靜態變量,避免頻繁創建它的對象實例,如下代碼:


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();
}
}
});
}
// 等待上述的線程執行完
service.shutdown();
service.awaitTermination(1, TimeUnit.DAYS);
}


控制臺打印結果:



你看這不崩了?部分線程獲取的時間不對,部分線程直接報java.lang.NumberFormatException: multiple points錯,線程直接掛死了。


多線程不安全原因


因為我們吧SimpleDateFormat定義為靜態變量,那么多線程下SimpleDateFormat的實例就會被多個線程共享,B線程會讀取到A線程的時間,就會出現時間差異和其它各種問題。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變量也就是一個共享變量,可以被多個線程訪問。


假設線程A執行完calendar.setTime(date),把時間設置成2019-01-02,這時候被掛起,線程B獲得CPU執行權。線程B也執行到了calendar.setTime(date),把時間設置為2019-01-03。線程掛起,線程A繼續走,calendar還會被繼續使用(subFormat方法),而這時calendar用的是線程B設置的值了,而這就是引發問題的根源,出現時間不對,線程掛死等等。


其實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.


意思就是

日期格式不同步。 

建議為每個線程創建單獨的格式實例。

如果多個線程同時訪問一種格式,則必須在外部同步該格式。


解決方案


只在需要的時候創建新實例,不用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);
}


如上代碼,僅在需要用到的地方創建一個新的實例,就沒有線程安全問題,不過也加重了創建對象的負擔,會頻繁地創建和銷毀對象,效率較低。


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往上一套也可以解決線程安全問題,缺點自然就是并發量大的時候會對性能有影響,線程阻塞。


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


也是《阿里巴巴開發手冊》給我們的解決方案,對之前的代碼進行改造:


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();
}
}
});
}
// 等待上述的線程執行完
service.shutdown();
service.awaitTermination(1, TimeUnit.DAYS);


}
}


運行結果就不貼了,不會出現報錯和時間不準確的問題。


DateTimeFormatter源碼上作者也加注釋說明了,他的類是不可變的,并且是線程安全的。


* This class is immutable and thread-safe.


我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(1)
天明向日葵 2019-5-20 14:04
學習了
回復
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

两码中特期期