平常開發中,咱們常常須要使用時間相關類,說到時間相關類,想必你們對 SimpleDateFormat 並不陌生。主要是用它進行時間的格式化輸出和解析,挺方便快捷的,可是 SimpleDateFormat 並非一個線程安全 的類。在多線程狀況下,會出現異常,想必有經驗的小夥伴也遇到過。下面咱們就來分析分析SimpleDateFormat爲何不安全?是怎麼引起的?以及多線程下有那些SimpleDateFormat的解決方案?java
先看看《阿里巴巴開發手冊》對於 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); }
控制檯打印結果:app
部分線程獲取的時間不對,部分線程直接報 java.lang.NumberFormatException:multiple points 錯,線程直接掛死了。ide
由於咱們把 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。spa
由於咱們聲明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.
日期格式不一樣步。建議爲每一個線程建立單獨的格式實例。 若是多個線程同時訪問一種格式,則必須在外部同步該格式。
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); }
如上代碼,僅在須要用到的地方建立一個新的實例,就沒有線程安全問題,不過也加劇了建立對象的負擔,會 頻繁地建立和銷燬對象,效率較低。
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 */ 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 的對象,那麼天然也就不存在競爭問題了。
也是《阿里巴巴開發手冊》給咱們的解決方案,對以前的代碼進行改造:
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源碼上做者也加註釋說明了,他的類是不可變的,而且是線程安全的。