1. 程式人生 > >轉:深入理解Java:SimpleDateFormat安全的時間格式化

轉:深入理解Java: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

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


相關推薦

深入理解JavaSimpleDateFormat安全時間格式化

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

深入理解Java註解(Annotation)--註解處理器

display 枚舉 lec con null cto run toolbar int https://www.cnblogs.com/peida/archive/2013/04/26/3038503.html   如果沒有用來讀取註解的方法和工作,那麽註解也就

深入理解JavaSimpleDateFormat安全時間格式化

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

深入理解Java G1垃圾收集器

線程數 hot 原因 重要 特性 賦值 圖標 參數 堆內存 java垃圾收集器的歷史 第一階段,Serial(串行)收集器 在jdk1.3.1之前,java虛擬機僅僅能使用Serial收集器。 Serial收集器是一個單線程的收集器,但它的“單線程”的意義並不僅僅是說明它只

深入理解Java類加載機制及反射

指定 請求 image vm虛擬機 常量池 使用 元素 靜態 創建 一、Java類加載機制 1.概述 Class文件由類裝載器裝載後,在JVM中將形成一份描述Class結構的元信息對象,通過該元信息對象可以獲知Class的結構信息:如構造函數,屬性和方法等,J

深入理解Java註解(Annotation)--註解處理器

fault this urn 復制代碼 lena ide set java lec 深入理解Java:註解(Annotation)--註解處理器   如果沒有用來讀取註解的方法和工作,那麽註解也就不會比註釋更有用處了。使用註解的過程中,很重要的一部分就是創建於

深入理解Java註解

註釋 element 每一個 gree arc res 參數名稱 生命周期 水果 註解作用:每當你創建描述符性質的類或者接口時,一旦其中包含重復性的工作,就可以考慮使用註解來簡化與自動化該過程。 Java提供了四種元註解,專門負責新註解的創建工作。‘ 元註解

java基礎學習總結(十二)深入理解java內部類

內部類 內部類也是語法糖,是因為它僅僅是一個編譯時的概念,outer.java裡面定義了一個內部類inner,一旦編譯成功,就會生成兩個完全不同的.class檔案了,分別是outer.class和outer$inner.class。所以內部類的名字完全可以和它的外部類名字相同。 內部類分為四

java基礎學習總結(九)深入理解Java泛型

一、什麼是泛型         “泛型” 意味著編寫的程式碼可以被不同型別的物件所重用。泛型的提出是為了編寫重用性更好的程式碼。泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數。 比如常見的集合類 LinkedList: publi

分散式系統的架構思路 深入理解java5. Java分散式架構

原文來源:https://www.cnblogs.com/chulung/p/5653135.html 一、前言 在計算機領域,當單機效能達到瓶頸時,有兩種方式可以解決效能問題,一是堆硬體,進一步提升配置,二是分散式,水平擴充套件。當然,兩者都是一樣的燒錢。今天聊聊我所理解的分散式系統的架構思路。

深入理解java執行緒本地變數 java.lang.ThreadLocal類

ThreadLocal,很多人都叫它做執行緒本地變數,也有些地方叫做執行緒本地儲存,其實意思差不多。 可能很多朋友都知道ThreadLocal為變數在每個執行緒中都建立了一個副本,那樣每個執行緒可以訪問自己內部的副本變數。 這句話從表面上看起來理解正確,但實際上這種理解是不太正確的。下面我們

Java併發指南2深入理解Java記憶體模型JMM

一:JMM基礎與happens-before 1併發程式設計模型的分類 1.1執行緒之間如何通訊及執行緒之間如何同步 1.11執行緒之間的通訊機制 通訊:執行緒之間以何種機制來交換資訊 通訊機制有兩種:共享記憶體和訊息傳遞。 共享記憶體併發模型 執行

Java基礎深入理解java異常處理機制的原理和開發應用【轉載】

Java異常處理機制在日常開發中應用頻繁,本篇文章主要在基礎的使用方法上,更進一步的,如何更加合理的使用異常機制,希望可以對各位朋友能有所幫助。   Java異常處理機制其最主要的幾個關鍵字:try、catch、finally、throw、throws,以及各種各樣

視訊深入理解Java虛擬機器(jvm效能調優+記憶體模型+虛擬機器原理)共110集

龍果學院深入理解Java虛擬機器(Jvm效能調優+記憶體模型+虛擬機器原視訊 Java虛擬機器視訊教程一套不錯的視訊,課程一共有110課,課程目錄較多隻展示部分出來,喜歡的朋友下載看下 課程目錄(課程較多,只展示部分目錄) 課程大綱 第1節說在前面的話 [免費觀看]

深入理解JavaString

args 學習 局部變量 its 建立 floating 分布式 生存 源碼 在講解String之前,我們先了解一下Java的內存結構。 一、Java內存模型 按照官方的說法:Java 虛擬機具有一個堆,堆是運行時數據區域,所有類實例和數組的內存均從此處分配。 JVM主要管

深入理解Java註解(Annotation)自定義註解

要深入學習註解,我們就必須能定義自己的註解,並使用註解,在定義自己的註解之前,我們就必須要了解Java為我們提供的元註解和相關定義註解的語法。 元註解:   元註解的作用就是負責註解其他註解。Java5.0定義了4個標準的meta-annotation型別,它們被用來

深入理解Java類載入機制及反射

 說明:本文乃學習整理參考而來. 一、Java類載入機制 1.概述        Class檔案由類裝載器裝載後,在JVM中將形成一份描述Class結構的元資訊物件,通過該元資訊物件可以獲知Class的結構資訊:如建構函式,屬性和方法等,Java允許使用者藉由這個Class相關的元資訊物件間接呼

Java基礎深入理解Java的介面和抽象類

     對於面向物件程式設計來說,抽象是它的一大特徵之一。在Java中,可以通過兩種形式來體現OOP的抽象:介面和抽象類。這兩者有太多相似的地方,又有太多不同的地方。很多人在初學的時候會以為它們可以隨意互換使用,但是實際則不然。今天我們就一起來學習一下Java中的介面和

深入理解Java註解(Annotation)自定義註解入門

要深入學習註解,我們就必須能定義自己的註解,並使用註解,在定義自己的註解之前,我們就必須要了解Java為我們提供的元註解和相關定義註解的語法。 元註解:   元註解的作用就是負責註解其他註解。Java5.0定義了4個標準的meta-annotation型別,它們被用來

學習筆記深入理解Java虛擬機 第二章:Java內存區域與內存溢出異常(1)

block 物理 裝載 成熟 memory from ram 權力 一個 學習筆記:深入理解Java虛擬機 第二章:Java內存區域與內存溢出異常(1) Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍成的"高墻",墻外面的人想進去,墻裏面的