1. 程式人生 > >SimpleDateFormat時間格式化存線上程安全問題

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

想必大家對SimpleDateFormat並不陌生。SimpleDateFormat 是 Java 中一個非常常用的類,該類用來對日期字串進行解析和格式化輸出,但如果使用不小心會導致非常微妙和難以除錯的問題,因為 DateFormat 和 SimpleDateFormat 類不都是執行緒安全的,在多執行緒環境下呼叫 format() 和 parse() 方法應該使用同步程式碼來避免問題。下面我們通過一個具體的場景來一步步的深入學習和理解SimpleDateFormat類。

一.引子
  我們都是優秀的程式設計師,我們都知道在程式中我們應當儘量少的建立SimpleDateFormat 例項,因為建立這麼一個例項需要耗費很大的代價。在一個讀取資料庫資料匯出到excel檔案的例子當中,每次處理一個時間資訊的時候,就需要建立一個SimpleDateFormat例項物件,然後再丟棄這個物件。大量的物件就這樣被創建出來,佔用大量的記憶體和 jvm空間。程式碼如下:

複製程式碼

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類(如下)中,在使用時直接使用這個例項進行操作,這樣問題就解決了。改進後的程式碼如下:

複製程式碼

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.

  下面我們通過看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類庫對時間處理方式比較完美,建議使用。

  參考資料:

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

相關推薦

Java多執行緒中static變數的使用 SimpleDateFormat時間格式化線上安全問題

兩篇文章 Java多執行緒中static變數的使用  (轉自:http://blog.csdn.net/yy304935305/article/details/52456771) &&  SimpleDateFormat時間格式化存線上程安全問題

SimpleDateFormat時間格式化線上安全問題

想必大家對SimpleDateFormat並不陌生。SimpleDateFormat 是 Java 中一個非常常用的類,該類用來對日期字串進行解析和格式化輸出,但如果使用不小心會導致非常微妙和難以除錯的問題,因為 DateFormat 和 SimpleDateFormat 類

SimpleDateFormat時間格式化執行緒不安全問題

想必大家對SimpleDateFormat並不陌生。SimpleDateFormat 是 Java 中一個非常常用的類,該類用來對日期字串進行解析和格式化輸出,但如果使用不小心會導致非常微妙和難以除錯的問題,因為 DateFormat 和 SimpleDateFormat 類

多執行緒中使用靜態方法線上安全的問題

   類的成員分兩類,靜態成員(static member)和例項成員(instance menber),靜態成員屬於類,例項成員則屬於物件,即類的例項。    我們知道,靜態欄位和靜態方法的呼叫都是通過類來呼叫的,靜態方法不會對特定的例項操作,只能呼叫呼叫類中的其他屬性和

servlet是否線上安全問題

        今天老師問了一句,servlet存線上程安全問題嗎,心想著servlet不是單例的嘛,每個執行緒在呼叫的時候都會為例項物件分配獨立的引用。我就以為servlet屬於執行緒安全的。晚上自己再網上查看了一下,發現servlet不是安全的。        下面就給大

SimpleDateFormat時間格式化執行緒不安全問題_2

1. 原因 SimpleDateFormat(下面簡稱sdf)類內部有一個Calendar物件引用,它用來儲存和這個sdf相關的日期資訊,例如sdf.parse(dateStr), sdf.format(date) 諸如此類的方法引數傳入的日期相關String, Date等

SimpleDateFormat時間格式化高併發、多執行緒時出現問題

SimpleDateFormat是是 Java 中一個非常常用的類,該類用來對日期字串進行解析和格式化輸出,但是DateFormat 和 SimpleDateFormat 類不都是執行緒安全的,在生產環境的多執行緒或高併發情況使用 format() 和 parse() 方法,會出現很多問題:

Java傳統的時間格式化的執行緒安全問題

以下程式碼會報錯: package com.expgiga.Java8; import java.text.SimpleDateFormat; import java.util.ArrayList

【JAVA8新的時間與日期 API】- 傳統時間格式化的執行緒安全問題

Java8之前的日期和時間API,存在一些問題,最重要的就是執行緒安全的問題。這些問題都在Java8中的日期和時間API中得到了解決,而且Java8中的日期和時間API更加強大。 傳統時間格式化的執行緒安全問題 示例: import java.text.SimpleDateFormat; import

(轉)關於SimpleDateFormat安全時間格式化安全問題

簡單 性能提升 calendar類 def 深入學習 日期 創建 它的 PE 想必大家對SimpleDateFormat並不陌生。SimpleDateFormat 是 Java 中一個非常常用的類,該類用來對日期字符串進行解析和格式化輸出,但如果使用不小心會導致非常微妙和難

深入理解Java:SimpleDateFormat安全時間格式化

  想必大家對SimpleDateFormat並不陌生。SimpleDateFormat 是 Java 中一個非常常用的類,該類用來對日期字串進行解析和格式化輸出,但如果使用不小心會導致非常微妙和難以除錯的問題,因為 DateFormat 和 SimpleDateForm

JAVA複習資料-關於SimpleDateFormat安全時間格式化執行緒安全問題

想必大家對SimpleDateFormat並不陌生。SimpleDateFormat 是 Java 中一個非常常用的類,該類用來對日期字串進行解析和格式化輸出,但如果使用不小心會導致非常微妙和難以除錯的問題,因為 DateFormat 和 SimpleDateFormat

轉:深入理解Java:SimpleDateFormat安全時間格式化

宣告:沒有冒犯原作者的意思,只是感覺這篇文章寫得很好,所以轉過來供大家學習,感謝作者的分享!!!  想必大家對SimpleDateFormat並不陌生。SimpleDateFormat 是 Java 中一個非常常用的類,該類用來對日期字串進行解析和格式化輸出,但如果

SimpleDateFormat安全時間格式化

想必大家對SimpleDateFormat並不陌生。SimpleDateFormat 是 Java 中一個非常常用的類,該類用來對日期字串進行解析和格式化輸出,但如果使用不小心會導致非常微妙和難以除錯的問題,因為 DateFormat 和 SimpleDateFormat

日期格式化SimpleDateFormat【線安全】、FastDateFormat和Joda-Time【後兩個都是線安全

apach hang 當前 detail ssa sha 需要 import tex SimpleDateFormat是線程不安全的,不能多個線程公用。而FastDateFormat和Joda-Time都是線程安全的,可以放心使用。 SimpleDateFormat是JD

Java並發程序設計(三) Java內模型和線安全

-h static tar -a 順序 語義 ret public font Java內存模型和線程安全 一 、原子性 原子性是指一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其它線程幹擾。 思考:i++是原子操作嗎? 二、有序性

Java SimpleDateFormat 中英文時間格式化轉換

這樣的 string類 pattern enter col english one 表示 匹配 SimpleDateFormat是一個以與語言環境有關的方式來格式化和解析日期的具體類。它允許進行格式化(日期 -> 文本)、解析(文本 -> 日期)和規範化。

ThreadLocal解決SimpleDateFormat多線安全問題中遇到的困惑

檢查 initial hashcode n) lan ext 線程安全 args [] 測試代碼: public class Main { public static void main(String[] args) { for (int

強大易用的日期和時間庫 線安全 Joda Time

class ... 進行 comment images 時間格式化 一個 ges 而且 https://www.ibm.com/developerworks/cn/java/j-jodatime.html 不可變性 我在本文討論的 Joda 類具有不可變性,因此它們的實例無

nginx優化,隱藏版本號,修改用戶和組,網頁緩時間,更改進數,以及防盜鏈

vim 改版 篡改 cli gin === rip code vpd 設置nginx優化隱藏版本號,修改用戶和組,網頁緩存時間,更改進程數, 以及防盜鏈的代碼內容 ========================隱藏版本號,修改用戶和組 方法一: cd /opt/nginx