SimpleDateFormat的相關學習踩坑記

轉載至:ImportNew的《還在使用 SimpleDateFormat ?你的項目崩沒?》

作者:周宇峯

突然看到ImportNew的相關推送,想起了以前踩過的坑,特此轉載順便記錄一下。

先看看《阿里巴巴開發手冊》對於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.

 

ok,現在是不是可以對你項目裏的日期工具類進行一波優化了呢?

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章