1. 程式人生 > >SimpleDateFormat並發隱患及其解決

SimpleDateFormat並發隱患及其解決

sub mon 兩種 pri 性能 toa 多線程並發 屬性 私有

此文已由作者姚太行授權網易雲社區發布。

歡迎訪問網易雲社區,了解更多網易技術產品運營經驗。


SimpleDateFormat被大量使用於處理時間格式化過程,由於該類在創建時會指定一個pattern用於標明固定的時間格式,所以在使用中,一般會創建一個作用域較大(static修飾或某類的私有屬性)的對象用於重復使用。由於時間轉換過程遇到的多線程並發的使用場景並不多見,所以很難發現在該類的隱患,事實上,該類並非是線程安全的,在多線程使用format()和parse()方法時可能會遇到問題。


分析

在SimpleDateFormat及其父類DateFormat的源文件裏,有這樣一段說明:


* 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.


JDK文檔中已經明確指出,這兩個類在進行時間格式化的過程中都是非線程安全的。也就是說,使用同一個SimpleDateFormat實例,開若幹線程做日期轉換操作,得到的結果可能並不準確。


parse

parse()測試,參考了其他人對此做的實驗,我使用的測試代碼(jdk1.8)如下:


import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class DateFormatTest extends Thread {    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");    private String name;    private String dateStr;    public DateFormatTest(String name, String dateStr) {        this.name = name;        this.dateStr = dateStr;
    }    @Override
    public void run() {

        Date date = null;        try {
            date = sdf.parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }

        System.out.println(name + " : date: " + date);
    }    public static void main(String[] args) throws InterruptedException {
        ExecutorService executor = Executors.newCachedThreadPool();

        executor.execute(new DateFormatTest("Test_A", "2000-04-28"));
        executor.execute(new DateFormatTest("Test_B", "2017-04-28"));

        executor.shutdown();
    }
}


這段測試代碼參考了網上一段用例,與之不同的是,原用例中在兩個線程操作中間做了線程等待Sleep,而為了看到效果,修改後的測試用例把線程等待的部分去掉。雖然每次運行的結果都會不太一樣,但經常會拋出的異常:


Exception in thread "pool-1-thread-1" java.lang.NumberFormatException: For input string: ""
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Long.parseLong(Long.java:601)
    at java.lang.Long.parseLong(Long.java:631)
    at java.text.DigitList.getLong(DigitList.java:195)
    at java.text.DecimalFormat.parse(DecimalFormat.java:2051)
    at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at DateFormatTest.run(DateFormatTest.java:24)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
Test_B : date: Mon Apr 28 00:00:00 CST 2200


需要明確的是,待轉換的字符串作為非靜態私有變量是每個對象持有的,只有sdf本身是公用的,不難發現即便是成功輸出了,但是數值也未必會是正確的,parse()方法不安全。


format

SimpleDateFormat的format()方法源碼如下:


private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {        // Convert input date to time field list
        calendar.setTime(date);
        ...


需要註意的是calendar的操作並非是線程安全的,很顯然在並發情景下,format的使用並不安全,測試過程與對parse過程的測試相似,不再贅述。


解決

既然SimpleDateFormat本身並不安全,那麽解決的方式無非兩種:優化使用過程或者找替代品。


1.臨時創建

不使用Static,每次使用時,創建新實例。


存在的問題:

SimpleDateFormat中使用了Calendar對象,由於該對象相當重,在高並發的情況下會大量的new SimpleDateFormat以及銷毀SimpleDateFormat,極其耗費資源。


2.synchronized

以synchronized同步SimpleDateFormat對象。


存在的問題:

高並發時,使用該對象會出現阻塞,當前使用者使用時,其他使用者等待,盡管結果是對的,但是並發成了排隊,實際上並沒有解決問題,還會對性能以及效率造成影響。


3.ThreadLocal

使用ThreadLocal,令每個線程創建一個當前線程的SimpleDateFormat的實例對象。


存在的問題:

使用ThreadLocal時,如果執行原子任務的過程是每一個線程執行一個任務,那麽這樣的聲明基本和每次使用前創建實例對象是沒區別的;如果使用的是多線程加任務隊列,舉個例子,tomcat有m個處理線程,外部有n個待處理任務請求,那麽當執行n個任務時,其實只會創建m個SimpleDateFormat實例,對於單一的處理線程,執行任務是有序的,所以對於當前線程而言,不存在並發。


4.Apache的 DateFormatUtils 與 FastDateFormat

使用org.apache.commons.lang.time.FastDateFormat 與 org.apache.commons.lang.time.DateFormatUtils。


存在的問題:

apache保證是線程安全的,並且更高效。但是DateFormatUtils與FastDateFormat這兩個類中只有format()方法,所有的format方法只接受long,Date,Calendar類型的輸入,轉換成時間串,目前不存在parse()方法,可由時間字符串轉換為時間對象。


5.Joda-Time

使用Joda-Time類庫。


存在的問題:

沒有問題~


簡介:

Joda-Time — 面向 Java 應用程序的日期/時間庫的替代選擇,Joda-Time 令時間和日期值變得易於管理、操作和理解。事實上,易於使用是 Joda 的主要設計目標。其他目標包括可擴展性、完整的特性集以及對多種日歷系統的支持。並且 Joda 與 JDK 是百分之百可互操作的,因此您無需替換所有 Java 代碼,只需要替換執行日期/時間計算的那部分代碼。


資料:

Joda-Time 簡介(中文)https://www.ibm.com/developerworks/cn/java/j-jodatime.html
Joda-Time 文檔(英文)http://joda-time.sourceforge.net/




“欲要看究竟,處處細留心。” —宋帆


免費體驗雲安全(易盾)內容安全、驗證碼等服務

更多網易技術、產品、運營經驗分享請點擊。


相關文章:
【推薦】 網易雲數據庫架構設計實踐
【推薦】 jq一個強悍的json格式化查看工具

SimpleDateFormat並發隱患及其解決