SimpleDateFormat時間格式化存在線程安全問題

  想必你們對SimpleDateFormat並不陌生。SimpleDateFormat 是 Java 中一個很是經常使用的類,該類用來對日期字符串進行解析和格式化輸出,但若是使用不當心會致使很是微妙和難以調試的問題,由於 DateFormat 和 SimpleDateFormat 類不都是線程安全的,在多線程環境下調用 format() 和 parse() 方法應該使用同步代碼來避免問題。下面咱們經過一個具體的場景來一步步的深刻學習和理解SimpleDateFormat類。html

一.引子
  咱們都是優秀的程序員,咱們都知道在程序中咱們應當儘可能少的建立SimpleDateFormat 實例,由於建立這麼一個實例須要耗費很大的代價。在一個讀取數據庫數據導出到excel文件的例子當中,每次處理一個時間信息的時候,就須要建立一個SimpleDateFormat實例對象,而後再丟棄這個對象。大量的對象就這樣被建立出來,佔用大量的內存和 jvm空間。代碼以下:java

package com.peidasoft.dateformat;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateUtil {
    
    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);
    }
}

  你也許會說,OK,那我就建立一個靜態的simpleDateFormat實例,而後放到一個DateUtil類(以下)中,在使用時直接使用這個實例進行操做,這樣問題就解決了。改進後的代碼以下:git

package com.peidasoft.dateformat;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateUtil {
    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);
    }
}

  固然,這個方法的確很不錯,在大部分的時間裏面都會工做得很好。但當你在生產環境中使用一段時間以後,你就會發現這麼一個事實:它不是線程安全的。在正常的測試狀況之下,都沒有問題,但一旦在生產環境中必定負載狀況下時,這個問題就出來了。他會出現各類不一樣的狀況,好比轉化的時間不正確,好比報錯,好比線程被掛死等等。咱們看下面的測試用例,那事實說話:程序員

package com.peidasoft.dateformat;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateUtil {
    
    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);
    }
}
package com.peidasoft.dateformat;

import java.text.ParseException;
import java.util.Date;

public class DateUtilTest {
    
    public static class TestSimpleDateFormatThreadSafe extends Thread {
        @Override
        public void run() {
            while(true) {
                try {
                    this.join(2000);
                } catch (InterruptedException e1) {
                    e1.printStackTrace();
                }
                try {
                    System.out.println(this.getName()+":"+DateUtil.parse("2013-05-24 06:02:20"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        }    
    }
    
    
    public static void main(String[] args) {
        for(int i = 0; i < 3; i++){
            new TestSimpleDateFormatThreadSafe().start();
        }
            
    }
}

  執行輸出以下:數據庫

Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082)
    at java.lang.Double.parseDouble(Double.java:510)
    at java.text.DigitList.getDouble(DigitList.java:151)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1302)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
    at java.text.DateFormat.parse(DateFormat.java:335)
    at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17)
    at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20)
Exception in thread "Thread-0" java.lang.NumberFormatException: multiple points
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082)
    at java.lang.Double.parseDouble(Double.java:510)
    at java.text.DigitList.getDouble(DigitList.java:151)
    at java.text.DecimalFormat.parse(DecimalFormat.java:1302)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311)
    at java.text.DateFormat.parse(DateFormat.java:335)
    at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17)
    at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20)
Thread-2:Mon May 24 06:02:20 CST 2021
Thread-2:Fri May 24 06:02:20 CST 2013
Thread-2:Fri May 24 06:02:20 CST 2013
Thread-2:Fri May 24 06:02:20 CST 2013

  說明:Thread-1和Thread-0報java.lang.NumberFormatException: multiple points錯誤,直接掛死,沒起來;Thread-2 雖然沒有掛死,但輸出的時間是有錯誤的,好比咱們輸入的時間是:2013-05-24 06:02:20 ,當會輸出:Mon May 24 06:02:20 CST 2021 這樣的靈異事件。緩存

  二.緣由安全

  做爲一個專業程序員,咱們固然都知道,相比於共享一個變量的開銷要比每次建立一個新變量要小不少。上面的優化過的靜態的SimpleDateFormat版,之所在併發狀況下回出現各類靈異錯誤,是由於SimpleDateFormat和DateFormat類不是線程安全的。咱們之因此忽視線程安全的問題,是由於從SimpleDateFormat和DateFormat類提供給咱們的接口上來看,實在讓人看不出它與線程安全有何相干。只是在JDK文檔的最下面有以下說明:多線程

  SimpleDateFormat中的日期格式不是同步的。推薦(建議)爲每一個線程建立獨立的格式實例。若是多個線程同時訪問一個格式,則它必須保持外部同步。併發

  JDK原始文檔以下:
  Synchronization:
  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.app

  下面咱們經過看JDK源碼來看看爲何SimpleDateFormat和DateFormat類不是線程安全的真正緣由:

  SimpleDateFormat繼承了DateFormat,在DateFormat中定義了一個protected屬性的 Calendar類的對象:calendar。只是由於Calendar累的概念複雜,牽扯到時區與本地化等等,Jdk的實現中使用了成員變量來傳遞參數,這就形成在多線程的時候會出現錯誤。

  在format方法裏,有這樣一段代碼:

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)這條語句改變了calendar,稍後,calendar還會用到(在subFormat方法裏),而這就是引起問題的根源。想象一下,在一個多線程環境下,有兩個線程持有了同一個SimpleDateFormat的實例,分別調用format方法:
  線程1調用format方法,改變了calendar這個字段。
  中斷來了。
  線程2開始執行,它也改變了calendar。
  又中斷了。
  線程1回來了,此時,calendar已然不是它所設的值,而是走上了線程2設計的道路。若是多個線程同時爭搶calendar對象,則會出現各類問題,時間不對,線程掛死等等。
  分析一下format的實現,咱們不難發現,用到成員變量calendar,惟一的好處,就是在調用subFormat時,少了一個參數,卻帶來了這許多的問題。其實,只要在這裏用一個局部變量,一路傳遞下去,全部問題都將迎刃而解。
  這個問題背後隱藏着一個更爲重要的問題--無狀態:無狀態方法的好處之一,就是它在各類環境下,均可以安全的調用。衡量一個方法是不是有狀態的,就看它是否改動了其它的東西,好比全局變量,好比實例的字段。format方法在運行過程當中改動了SimpleDateFormat的calendar字段,因此,它是有狀態的。

  這也同時提醒咱們在開發和設計系統的時候注意下一下三點:

  1.本身寫公用類的時候,要對多線程調用狀況下的後果在註釋裏進行明確說明

  2.對線程環境下,對每個共享的可變變量都要注意其線程安全性

  3.咱們的類和方法在作設計的時候,要儘可能設計成無狀態的

  三.解決辦法

  1.須要的時候建立新實例:

package com.peidasoft.dateformat;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateUtil {
    
    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);
    }
}

  說明:在須要用到SimpleDateFormat 的地方新建一個實例,無論何時,將有線程安全問題的對象由共享變爲局部私有都能避免多線程問題,不過也加劇了建立對象的負擔。在通常狀況下,這樣其實對性能影響比不是很明顯的。

  2.使用同步:同步SimpleDateFormat對象

package com.peidasoft.dateformat;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class DateSyncUtil {

    private static 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);
        }
    } 
}

  說明:當線程較多時,當一個線程調用該方法時,其餘想要調用此方法的線程就要block,多線程併發量大的時候會對性能有必定的影響。

  3.使用ThreadLocal: 

package com.peidasoft.dateformat;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class ConcurrentDateUtil {

    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);
    }
}

  另一種寫法:

package com.peidasoft.dateformat;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class ThreadLocalDateUtil {
    private static final String date_format = "yyyy-MM-dd HH:mm:ss";
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(); 
 
    public static DateFormat getDateFormat()   
    {  
        DateFormat df = threadLocal.get();  
        if(df==null){  
            df = new SimpleDateFormat(date_format);  
            threadLocal.set(df);  
        }  
        return df;  
    }  

    public static String formatDate(Date date) throws ParseException {
        return getDateFormat().format(date);
    }

    public static Date parse(String strDate) throws ParseException {
        return getDateFormat().parse(strDate);
    }   
}

  說明:使用ThreadLocal, 也是將共享變量變爲獨享,線程獨享確定能比方法獨享在併發環境中能減小很多建立對象的開銷。若是對性能要求比較高的狀況下,通常推薦使用這種方法。

  4.拋棄JDK,使用其餘類庫中的時間格式化類:

  1.使用Apache commons 裏的FastDateFormat,宣稱是既快又線程安全的SimpleDateFormat, 惋惜它只能對日期進行format, 不能對日期串進行解析。

  2.使用Joda-Time類庫來處理時間相關問題

   

  作一個簡單的壓力測試,方法一最慢,方法三最快,可是就算是最慢的方法一性能也不差,通常系統方法一和方法二就能夠知足,因此說在這個點很難成爲你係統的瓶頸所在。從簡單的角度來講,建議使用方法一或者方法二,若是在必要的時候,追求那麼一點性能提高的話,能夠考慮用方法三,用ThreadLocal作緩存。

  Joda-Time類庫對時間處理方式比較完美,建議使用。

  參考資料:

  1.http://dreamhead.blogbus.com/logs/215637834.html

  2.http://www.blogjava.net/killme2008/archive/2011/07/10/354062.html

 

出處:http://www.cnblogs.com/peida/archive/2013/05/31/3070790.html

相關文章
相關標籤/搜索