1. 程式人生 > >SimpleDateFormat 如何安全的使用?

SimpleDateFormat 如何安全的使用?

怎麽 trac 字符串類 的確 一個 建議 dig class ins

技術分享圖片

前言

為什麽會寫這篇文章?因為這些天在看《阿裏巴巴開發手冊詳盡版》,沒看過的可以關註微信公眾號:zhisheng,回復關鍵字:阿裏巴巴開發手冊詳盡版 就可以獲得。

關註我

技術分享圖片

轉載請務必註明原創地址為:http://www.54tianzhisheng.cn/2018/06/19/SimpleDateFormat/

在看的過程中有這麽一條:

【強制】SimpleDateFormat 是線程不安全的類,一般不要定義為 static 變量,如果定義為 static,必須加鎖,或者使用 DateUtils 工具類。

看到這條我立馬就想起了我實習的時候有個項目裏面就犯了這個錯誤,記得當時是這樣寫的:

private
static final SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");

所以才認真的去研究下這個 SimpleDateFormat,所以才有了這篇文章。

它是誰?

想必大家對 SimpleDateFormat 並不陌生。SimpleDateFormat 是 Java 中一個非常常用的類,他是以區域敏感的方式格式化和解析日期的具體類。 它允許格式化 (date -> text)、語法分析 (text -> date)和標準化。

SimpleDateFormat 允許以任何用戶指定的日期-時間格式方式啟動。 但是,建議使用 DateFormat

中的 getTimeInstancegetDateInstancegetDateTimeInstance 方法來創建一個日期-時間格式。 這幾個方法會返回一個默認的日期/時間格式。 你可以根據需要用 applyPattern 方法修改格式方式。

日期時間格式

日期和時間格式由 日期和時間模式字符串 指定。在 日期和時間模式字符串 中,未加引號的字母 ‘A‘ 到 ‘Z‘ 和 ‘a‘ 到 ‘z‘ 被解釋為模式字母,用來表示日期或時間字符串元素。文本可以使用單引號 (‘) 引起來,以免進行解釋。所有其他字符均不解釋,只是在格式化時將它們簡單復制到輸出字符串。

簡單的講:這些 A ——Z,a —— z 這些字母(不被單引號包圍的)會被特殊處理替換為對應的日期時間,其他的字符串還是原樣輸出。

日期和時間模式(註意大小寫,代表的含義是不同的)如下:

技術分享圖片

怎麽使用?

日期/時間格式模版樣例:(給的時間是:2001-07-04 12:08:56 U.S. Pacific Time time zone)

技術分享圖片

使用方法:

import java.text.SimpleDateFormat;
import java.util.Date;
/**
 * Created by zhisheng_tian on 2018/6/19
 */
public class FormatDateTime {
    public static void main(String[] args) {
        SimpleDateFormat myFmt = new SimpleDateFormat("yyyy年MM月dd日 HH時mm分ss秒");
        SimpleDateFormat myFmt1 = new SimpleDateFormat("yy/MM/dd HH:mm");
        SimpleDateFormat myFmt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//等價於now.toLocaleString()
        SimpleDateFormat myFmt3 = new SimpleDateFormat("yyyy年MM月dd日 HH時mm分ss秒 E ");
        SimpleDateFormat myFmt4 = new SimpleDateFormat("一年中的第 D 天 一年中第w個星期 一月中第W個星期 在一天中k時 z時區");
        Date now = new Date();
        System.out.println(myFmt.format(now));
        System.out.println(myFmt1.format(now));
        System.out.println(myFmt2.format(now));
        System.out.println(myFmt3.format(now));
        System.out.println(myFmt4.format(now));
        System.out.println(now.toGMTString());
        System.out.println(now.toLocaleString());
        System.out.println(now.toString());
    }
}

結果是:

2018年06月19日 23時10分05秒
18/06/19 23:10
2018-06-19 23:10:05
2018年06月19日 23時10分05秒 星期二
一年中的第 170 天 一年中第25個星期 一月中第4個星期 在一天中23時 CST時區
19 Jun 2018 15:10:05 GMT
2018-6-19 23:10:05
Tue Jun 19 23:10:05 CST 2018

使用方法很簡單,就是先自己定義好時間/日期模版,然後調用 format 方法(傳入一個時間 Date 參數)。

上面的是日期轉換成自己想要的字符串格式。下面反過來,將字符串類型裝換成日期類型:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
 * Created by zhisheng_tian on 2018/6/19
 */
public class StringFormatDate {

    public static void main(String[] args) {
        String time1 = "2018年06月19日 23時10分05秒";
        String time2 = "18/06/19 23:10";
        String time3 = "2018-06-19 23:10:05";
        String time4 = "2018年06月19日 23時10分05秒 星期二";

        SimpleDateFormat myFmt = new SimpleDateFormat("yyyy年MM月dd日 HH時mm分ss秒");
        SimpleDateFormat myFmt1 = new SimpleDateFormat("yy/MM/dd HH:mm");
        SimpleDateFormat myFmt2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//等價於now.toLocaleString()
        SimpleDateFormat myFmt3 = new SimpleDateFormat("yyyy年MM月dd日 HH時mm分ss秒 E");

        Date date1 = null;
        try {
            date1 = myFmt.parse(time1);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        System.out.println(date1);

        Date date2 = null;
        try {
            date2 = myFmt1.parse(time2);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        System.out.println(date2);

        Date date3 = null;
        try {
            date3 = myFmt2.parse(time3);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        System.out.println(date3);

        Date date4 = null;
        try {
            date4 = myFmt3.parse(time4);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        System.out.println(date4);
    }
}

結果是:

Tue Jun 19 23:10:05 CST 2018
Tue Jun 19 23:10:00 CST 2018
Tue Jun 19 23:10:05 CST 2018
Tue Jun 19 23:10:05 CST 2018

這個轉換方法也很簡單。但是不要高興的太早,主角不在這。

線程不安全

技術分享圖片

在 SimpleDateFormat 類的 JavaDoc 中,描述了該類不能夠保證線程安全,建議為每個線程創建單獨的日期/時間格式實例,如果多個線程同時訪問一個日期/時間格式,它必須在外部進行同步。那麽在多線程環境下調用 format() 和 parse() 方法應該使用同步代碼來避免問題。下面我們通過一個具體的場景來一步步的深入學習和理解SimpleDateFormat 類。

1、每個線程創建單獨的日期/時間格式實例

大量的創建 SimpleDateFormat 實例對象,然後再丟棄這個對象,占用大量的內存和 JVM 空間。

2、創建一個靜態的 SimpleDateFormat 實例,在使用時直接使用這個實例進行操作(我當時就是這麽幹的??)

private static final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date();
df.format(date);

當然,這個方法的確很不錯,在大部分的時間裏面都會工作得很好,但一旦在生產環境中一定負載情況下時,這個問題就出來了。他會出現各種不同的情況,比如轉化的時間不正確,比如報錯,比如線程被掛死等等。我們看下面的測試用例,拿事實說話:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
 * Created by zhisheng_tian on 2018/6/20
 */
public class DateUtils {
    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);
    }
}
import java.text.ParseException;
/**
 * Created by zhisheng_tian on 2018/6/20
 */
public class DateUtilsTest {
    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() + ":" + DateUtils.parse("2018-06-20 01:18: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-0" Exception in thread "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 com.zhisheng.demo.date.DateUtils.parse(DateUtils.java:19)
    at com.zhisheng.demo.date.DateUtilsTest$TestSimpleDateFormatThreadSafe.run(DateUtilsTest.java:19)
java.lang.NumberFormatException: For input string: ".1818"
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Long.parseLong(Long.java:578)
    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:2162)
    at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)
    at java.text.DateFormat.parse(DateFormat.java:364)
    at com.zhisheng.demo.date.DateUtils.parse(DateUtils.java:19)
    at com.zhisheng.demo.date.DateUtilsTest$TestSimpleDateFormatThreadSafe.run(DateUtilsTest.java:19)
Thread-2:Sat Jun 20 01:18:20 CST 2201
Thread-2:Wed Jun 20 01:18:20 CST 2018
Thread-2:Wed Jun 20 01:18:20 CST 2018
Thread-2:Wed Jun 20 01:18:20 CST 2018

說明:Thread-1和Thread-0報java.lang.NumberFormatException: multiple points錯誤,直接掛死,沒起來;Thread-2 雖然沒有掛死,但輸出的時間是有錯誤的,比如我們輸入的時間是:2018-06-20 01:18:20 ,當會輸出:Sat Jun 20 01:18:20 CST 2201 這樣的靈異事件。

Why?

為什麽會出現線程不安全的問題呢?

下面我們通過看 JDK 源碼來看看為什麽 SimpleDateFormat 和 DateFormat 類不是線程安全的真正原因:

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

在 SimpleDateFormat 中的 format 方法源碼中:

@Override
public StringBuffer format(Date date, StringBuffer toAppendTo,FieldPosition pos) {
  pos.beginIndex = pos.endIndex = 0;
  return format(date, toAppendTo, pos.getFieldDelegate());
}
// Called from Format after creating a FieldDelegate
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 這個字段。
線程 1 中斷了。
線程 2 開始執行,它也改變了 calendar。
線程 2 中斷了。
線程 1 回來了

此時,calendar 已然不是它所設的值,而是走上了線程 2 設計的道路。如果多個線程同時爭搶 calendar 對象,則會出現各種問題,時間不對,線程掛死等等。

分析一下 format 的實現,我們不難發現,用到成員變量 calendar,唯一的好處,就是在調用 subFormat 時,少了一個參數,卻帶來了許多的問題。其實,只要在這裏用一個局部變量,一路傳遞下去,所有問題都將迎刃而解。

這個問題背後隱藏著一個更為重要的問題--無狀態:無狀態方法的好處之一,就是它在各種環境下,都可以安全的調用。衡量一個方法是否是有狀態的,就看它是否改動了其它的東西,比如全局變量,比如實例的字段。format 方法在運行過程中改動了 SimpleDateFormat 的 calendar 字段,所以,它是有狀態的。

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

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

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

3.我們的類和方法在做設計的時候,要盡量設計成無狀態的

解決方法

1、需要的時候創建新實例

說明:在需要用到 SimpleDateFormat 的地方新建一個實例,不管什麽時候,將有線程安全問題的對象由共享變為局部私有都能避免多線程問題,不過也加重了創建對象的負擔。在一般情況下,這樣其實對性能影響比不是很明顯的。

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

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

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

說明:使用 ThreadLocal, 也是將共享變量變為獨享,線程獨享肯定能比方法獨享在並發環境中能減少不少創建對象的開銷。如果對性能要求比較高的情況下,一般推薦使用這種方法。

Java 8 中的解決辦法

Java 8 提供了新的日期時間 API,其中包括用於日期時間格式化的 DateTimeFormatter,它與 SimpleDateFormat 最大的區別在於:DateTimeFormatter 是線程安全的,而 SimpleDateFormat 並不是線程安全。

DateTimeFormatter 如何使用:

解析日期

String dateStr= "2018年06月20日";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年MM月dd日");   
LocalDate date= LocalDate.parse(dateStr, formatter);

日期轉換為字符串

LocalDateTime now = LocalDateTime.now();  
DateTimeFormatter format = DateTimeFormatter.ofPattern("yyyy年MM月dd日 hh:mm a");
String nowStr = now .format(format);

由 DateTimeFormatter 的靜態方法 ofPattern() 構建日期格式,LocalDateTime 和 LocalDate 等一些表示日期或時間的類使用 parse 和 format 方法把日期和字符串做轉換。

使用新的 API,整個轉換過程都不需要考慮線程安全的問題。

總結

SimpleDateFormat 是線程不安全的類,多線程環境下註意線程安全問題,如果是 Java 8 ,建議使用 DateTimeFormatter 代替 SimpleDateFormat。

參考資料

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

相關文章

20 個案例教你在 Java 8 中如何處理日期和時間?

SimpleDateFormat 如何安全的使用?