還在使用SimpleDateFormat?你的專案崩沒?

Java3y發表於2019-02-13

文字公眾號來源:Felix周(id:felix_space)  作者:Felix 

一篇關於SimpleDateFormat乾貨分享給大家!

還在使用SimpleDateFormat?你的專案崩沒?


一  前言

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

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

還在使用SimpleDateFormat?你的專案崩沒?

二   問題場景復現

一般我們使用SimpleDateFormat的時候會把它定義為一個靜態變數,避免頻繁建立它的物件例項,如下程式碼:

  1. public class SimpleDateFormatTest {


  2.    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


  3.    public static String formatDate(Date date) throws ParseException {

  4.        return sdf.format(date);

  5.    }


  6.    public static Date parse(String strDate) throws ParseException {

  7.        return sdf.parse(strDate);

  8.    }


  9.    public static void main(String[] args) throws InterruptedException, ParseException {


  10.        System.out.println(sdf.format(new Date()));


  11.    }

  12. }

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


  1.    public static void main(String[] args) throws InterruptedException, ParseException {



  2.        ExecutorService service = Executors.newFixedThreadPool(100);


  3.        for (int i = 0; i < 20; i++) {

  4.            service.execute(() -> {

  5.                for (int j = 0; j < 10; j++) {

  6.                    try {

  7.                        System.out.println(parse("2018-01-02 09:45:59"));

  8.                    } catch (ParseException e) {

  9.                        e.printStackTrace();

  10.                    }

  11.                }

  12.            });

  13.        }

  14.        // 等待上述的執行緒執行完

  15.        service.shutdown();

  16.        service.awaitTermination(1, TimeUnit.DAYS);

  17.    }

控制檯列印結果:

還在使用SimpleDateFormat?你的專案崩沒?

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

三   多執行緒不安全原因

因為我們把SimpleDateFormat定義為靜態變數,那麼多執行緒下SimpleDateFormat的例項就會被多個執行緒共享,B執行緒會讀取到A執行緒的時間,就會出現時間差異和其它各種問題。SimpleDateFormat和它繼承的DateFormat類也不是執行緒安全的

來看看SimpleDateFormatformat()方法的原始碼

  1.    // Called from Format after creating a FieldDelegate

  2.    private StringBuffer format(Date date, StringBuffer toAppendTo,

  3.                                FieldDelegate delegate) {

  4.        // Convert input date to time field list

  5.        calendar.setTime(date);


  6.        boolean useDateFormatSymbols = useDateFormatSymbols();


  7.        for (int i = 0; i < compiledPattern.length; ) {

  8.            int tag = compiledPattern[i] >>> 8;

  9.            int count = compiledPattern[i++] & 0xff;

  10.            if (count == 255) {

  11.                count = compiledPattern[i++] << 16;

  12.                count |= compiledPattern[i++];

  13.            }


  14.            switch (tag) {

  15.            case TAG_QUOTE_ASCII_CHAR:

  16.                toAppendTo.append((char)count);

  17.                break;


  18.            case TAG_QUOTE_CHARS:

  19.                toAppendTo.append(compiledPattern, i, count);

  20.                i += count;

  21.                break;


  22.            default:

  23.                subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols);

  24.                break;

  25.            }

  26.        }

  27.        return toAppendTo;

  28.    }

注意, calendar.setTime(date)SimpleDateFormatformat方法實際操作的就是Calendar

因為我們宣告SimpleDateFormatstatic變數,那麼它的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修飾

  1.    public static String formatDate(Date date) throws ParseException {

  2.        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  3.        return sdf.format(date);

  4.    }


  5.    public static Date parse(String strDate) throws ParseException {

  6.        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  7.        return sdf.parse(strDate);

  8.    }

如上程式碼,僅在需要用到的地方建立一個新的例項,就沒有執行緒安全問題,不過也加重了建立物件的負擔,會頻繁地建立和銷燬物件,效率較低

synchronized大法好

  1.    private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


  2.    public static String formatDate(Date date) throws ParseException {

  3.        synchronized(sdf){

  4.            return sdf.format(date);

  5.        }

  6.    }


  7.    public static Date parse(String strDate) throws ParseException {

  8.        synchronized(sdf){

  9.            return sdf.parse(strDate);

  10.        }

  11.    }

簡單粗暴,synchronized往上一套也可以解決執行緒安全問題,缺點自然就是併發量大的時候會對效能有影響,執行緒阻塞

ThreadLocal

  1.    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {

  2.        @Override

  3.        protected DateFormat initialValue() {

  4.            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

  5.        }

  6.    };


  7.    public static Date parse(String dateStr) throws ParseException {

  8.        return threadLocal.get().parse(dateStr);

  9.    }


  10.    public static String format(Date date) {

  11.        return threadLocal.get().format(date);

  12.    }

ThreadLocal可以確保每個執行緒都可以得到單獨的一個SimpleDateFormat的物件,那麼自然也就不存在競爭問題了。

基於JDK1.8的DateTimeFormatter

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

  1. public class SimpleDateFormatTest {


  2.    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");


  3.    public static String formatDate2(LocalDateTime date) {

  4.        return formatter.format(date);

  5.    }


  6.    public static LocalDateTime parse2(String dateNow) {

  7.        return LocalDateTime.parse(dateNow, formatter);

  8.    }


  9.    public static void main(String[] args) throws InterruptedException, ParseException {


  10.        ExecutorService service = Executors.newFixedThreadPool(100);


  11.        // 20個執行緒

  12.        for (int i = 0; i < 20; i++) {

  13.            service.execute(() -> {

  14.                for (int j = 0; j < 10; j++) {

  15.                    try {

  16.                        System.out.println(parse2(formatDate2(LocalDateTime.now())));

  17.                    } catch (Exception e) {

  18.                        e.printStackTrace();

  19.                    }

  20.                }

  21.            });

  22.        }

  23.        // 等待上述的執行緒執行完

  24.        service.shutdown();

  25.        service.awaitTermination(1, TimeUnit.DAYS);



  26.    }

  27. }

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

DateTimeFormatter原始碼上作者也加註釋說明了,他的類是不可變的,並且是執行緒安全的。

* This class is immutable and thread-safe.

OK,現在是不是可以對你專案裡的日期工具類進行一波最佳化了呢?

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69900354/viewspace-2629912/,如需轉載,請註明出處,否則將追究法律責任。

相關文章