java 編程中 Date 與 SimpleDateFormat 時間轉換不一致問題 與 SimpleDateFormat 線程安全問題

前提說明:

         java.util.Date中的getTime函數定義如下:

     java.util.Date代表一個時間點,其值爲距公元1970年1月1日 00:00:00的毫秒數。所以它是沒有時區和Locale概念的。

     public long getTime() 返回自 1970 年 1 月 1 日 00:00:00 GMT 以來此 Date 對象表示的毫秒數

  java中通過如下形式取得當前時間點: 

Date now =new Date(); //這個時間點與本地系統的時區無關,故不同時區同一時間獲取的Date數據時間戳是一致的。

  而正因爲其與時區的無關性,才使得我們的存儲數據(如:數據庫中的時間datetime類型數據)是一致的。

問題出現原因:不同時區服務器 連接統一數據庫 獲取時間 Date 數據是一致的,但是 由於服務器所在時區的不同,使用如下代碼進行字符串轉換的時候,出現不同時區服務器SimpleDateFormat轉換出的字符串不一致問題。

SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

String snow = sdf.format(now);

提示:上面代碼SimpleDateFormat 沒有設定時區屬性,會自動獲取當前服務器所在時區,進行字符串的轉換,進而造成時間字符串顯示的不同。

解決:

SimpleDateFormat sdf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

sdf.setTimeZone(TimeZone.getTimeZone("GMT+8"));

String snow = sdf.format(now);// snow = 2011-12-04 21:22:24

sdf.setTimeZone(TimeZone.getTimeZone("GMT+7"));

String snow2 = sdf.format(now);// snow2 = 2011-12-04 20:22:24 (可見:東八區比東七區早一個小時)

  另外,你可以通過如下代碼修改本地時區信息:

TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));

安全問題:

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.我們的類和方法在做設計的時候,要儘量設計成無狀態的

 

解決:

一:方法內 創建一個臨時 SimpleDateFormat, 使用完丟棄掉:頻繁創建對象消耗大,性能影響一些(推薦)。 

二:維護一個SimpleDateFormat實體,轉換方法上使用 Synchronized 保證線程安全:多線程堵塞(併發大系統不推薦)。

三:使用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);
    }
}

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章