項目中 SimpleDateFormat 的正確使用


項目中 SimpleDateFormat 的正確使用

日常開發中,我們經常需要使用時間相關類,說到時間相關類,想必大家對 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 {
        /** 單線程下測試. */
        System.out.println(sdf.format(new Date()));
    }
}

是不是感覺沒什么毛病?單線程下自然沒毛病了,都是運用到多線程下就有大問題了。 測試下:

    public static void main(String[] args) throws InterruptedException {
        /** 單線程下測試. */
        System.out.println(sdf.format(new Date()));

        /** 多線程下測試. */
        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 大法好

    public static String formatDate(Date date) throws ParseException {
        //return sdf.format(date);
        synchronized (sdf) {
            return sdf.format(date);
        }
    }
    public static Date parse(String strDate) throws ParseException {
        //return sdf.parse(strDate);
        synchronized (sdf) {
            return sdf.parse(strDate);
        }
    }

簡單粗暴,synchronized往上一套也可以解決線程安全問題,缺點自然就是並發量大的時候會對性能有影響,線程阻塞。

ThreadLocal

    /* ThreadLocal */
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static String formatDate(Date date) throws ParseException {
        //return sdf.format(date);
        /*synchronized (sdf) {
            return sdf.format(date);
        }*/
        return threadLocal.get().format(date);
    }

    public static Date parse(String strDate) throws ParseException {
        //return sdf.parse(strDate);
        /*synchronized (sdf) {
            return sdf.parse(strDate);
        }*/
        return threadLocal.get().parse(strDate);
    }

ThreadLocal 可以確保每個線程都可以得到單獨的一個 SimpleDateFormat 的對象,那么自然也就不存在競爭問題了。

基於JDK1.8的DateTimeFormatter

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

public class SimpleDateFormatTest8 {
    // 新建 DateTimeFormatter 類
    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    private static String formatDate(LocalDateTime dateTime) {
        return FORMATTER.format(dateTime);
    }

    private static LocalDateTime parse(String dateNow) {
        return LocalDateTime.parse(dateNow, FORMATTER);
    }

    public static void main(String[] args) throws InterruptedException {

        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(formatDate(LocalDateTime.now())));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        // 等待上述的線程執行完后
        service.shutdown();
        service.awaitTermination(1, TimeUnit.DAYS);
    }
}

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM