當前位置: 妍妍網 > 碼農

寫了這麽多年DateUtils,殊不知你還有這麽多彎彎繞!

2023-11-10碼農

(給哪咤編程加星標,提高Java技能)

大家好,我是哪咤。

在日常開發中,Date工具類使用頻率相對較高,大家通常都會這樣寫:

publicstatic Date getData(String date)throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(date);
}
publicstatic Date getDataByFormat(String date, String format)throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat(format);
return sdf.parse(date);
}

這很簡單啊,有什麽爭議嗎?

你應該聽過「時區」這個名詞,大家也都知道,相同時刻不同時區的時間是不一樣的。

因此在使用時間時,一定要給出時區資訊。

publicstaticvoidgetDataByZone(String param, String format)throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat(format);
// 預設時區解析時間表示
Date date = sdf.parse(param);
System.out.println(date + ":" + date.getTime());
// 東京時區解析時間表示
sdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo"));
Date newYorkDate = sdf.parse(param);
System.out.println(newYorkDate + ":" + newYorkDate.getTime());
}
publicstaticvoidmain(String[] args)throws ParseException {
getDataByZone("2023-11-10 10:00:00","yyyy-MM-dd HH:mm:ss");
}

對於當前的上海時區和紐約時區,轉化為 UTC 時間戳是不同的時間。

對於同一個本地時間的表示,不同時區的人解析得到的 UTC 時間一定是不同的,反過來不同的本地時間可能對應同一個 UTC。

格式化後出現的時間錯亂。

publicstaticvoidgetDataByZoneFormat(String param, String format)throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat(format);
Date date = sdf.parse(param);
// 預設時區格式化輸出
System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date));
// 東京時區格式化輸出
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));
System.out.println(new SimpleDateFormat("[yyyy-MM-dd HH:mm:ss Z]").format(date));
}
publicstaticvoidmain(String[] args)throws ParseException {
getDataByZoneFormat("2023-11-10 10:00:00","yyyy-MM-dd HH:mm:ss");
}

我當前時區的 Offset(時差)是 +8 小時,對於 +9 小時的紐約,整整差了1個小時,北京早上 10 點對應早上東京 11 點。

看看Java 8是如何解決時區問題的:

Java 8 推出了新的時間日期類 ZoneId、ZoneOffset、LocalDateTime、ZonedDateTime 和 DateTimeFormatter,處理時區問題更簡單清晰。

publicstaticvoidgetDataByZoneFormat8(String param, String format)throws ParseException {
ZoneId zone = ZoneId.of("Asia/Shanghai");
ZoneId tokyoZone = ZoneId.of("Asia/Tokyo");
ZoneId timeZone = ZoneOffset.ofHours(2);
// 格式化器
DateTimeFormatter dtf = DateTimeFormatter.ofPattern(format);
ZonedDateTime date = ZonedDateTime.of(LocalDateTime.parse(param, dtf), zone);
// withZone設定時區
DateTimeFormatter dtfz = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z");
System.out.println(dtfz.withZone(zone).format(date));
System.out.println(dtfz.withZone(tokyoZone).format(date));
System.out.println(dtfz.withZone(timeZone).format(date));
}
publicstaticvoidmain(String[] args)throws ParseException {
getDataByZoneFormat8("2023-11-10 10:00:00","yyyy-MM-dd HH:mm:ss");
}

  • Asia/Shanghai對應+8,對應2023-11-10 10:00:00;

  • Asia/Tokyo對應+9,對應2023-11-10 11:00:00;

  • timeZone 是+2,所以對應2023-11-10 04:00:00;

  • 在處理帶時區的國際化時間問題,推薦使用jdk8的日期時間類:

    1. 透過ZoneId,定義時區;

    2. 使用ZonedDateTime保存時間;

    3. 透過withZone對DateTimeFormatter設定時區;

    4. 進行時間格式化得到本地時間;

    思路比較清晰,不容易出錯。

    在與前端聯調時,報了個錯, java.lang.NumberFormatException: multiple points ,起初我以為是時間格式傳的不對,仔細一看,不對啊。

    百度一下,才知道是高並行情況下SimpleDateFormat有執行緒安全的問題。

    下面透過模擬高並行,把這個問題復現一下:

    publicstaticvoidgetDataByThread(String param, String format)throws InterruptedException {
    ExecutorService threadPool = Executors.newFixedThreadPool(5);
    SimpleDateFormat sdf = new SimpleDateFormat(format);
    // 模擬並行環境,開啟5個並行執行緒
    for (int i = 0; i < 5; i++) {
    threadPool.execute(() -> {
    for (int j = 0; j < 2; j++) {
    try {
    System.out.println(sdf.parse(param));
    catch (ParseException e) {
    System.out.println(e);
    }
    }
    });
    }
    threadPool.shutdown();
    threadPool.awaitTermination(1, TimeUnit.HOURS);
    }

    果不其然,報錯。還將2023年轉換成2220年,我勒個乖乖。

    在時間工具類裏,時間格式化,我都是這樣弄的啊,沒問題啊,為啥這個不行?原來是因為共用了同一個SimpleDateFormat,在工具類裏,一個執行緒一個SimpleDateFormat,當然沒問題啦!

    可以透過TreadLocal 局部變量,解決SimpleDateFormat的執行緒安全問題。

    publicstaticvoidgetDataByThreadLocal(String time, String format)throws InterruptedException {
    ExecutorService threadPool = Executors.newFixedThreadPool(5);
    ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>() {
    @Override
    protected SimpleDateFormat initialValue(){
    returnnew SimpleDateFormat(format);
    }
    };
    // 模擬並行環境,開啟5個並行執行緒
    for (int i = 0; i < 5; i++) {
    threadPool.execute(() -> {
    for (int j = 0; j < 2; j++) {
    try {
    System.out.println(sdf.get().parse(time));
    catch (ParseException e) {
    System.out.println(e);
    }
    }
    });
    }
    threadPool.shutdown();
    threadPool.awaitTermination(1, TimeUnit.HOURS);
    }

    看一下 SimpleDateFormat.parse 的源碼:

    public classSimpleDateFormatextendsDateFormat{
    @Override
    public Date parse(String text, ParsePosition pos){
    CalendarBuilder calb = new CalendarBuilder();
    Date parsedDate;
    try {
    parsedDate = calb.establish(calendar).getTime();
    // If the year value is ambiguous,
    // then the two-digit year == the default start year
    if (ambiguousYear[0]) {
    if (parsedDate.before(defaultCenturyStart)) {
    parsedDate = calb.addYear(100).establish(calendar).getTime();
    }
    }
    }
     }
    }
    classCalendarBuilder{
    Calendar establish(Calendar cal){
    boolean weekDate = isSet(WEEK_YEAR)
    && field[WEEK_YEAR] > field[YEAR];
    if (weekDate && !cal.isWeekDateSupported()) {
    // Use YEAR instead
    if (!isSet(YEAR)) {
    set(YEAR, field[MAX_FIELD + WEEK_YEAR]);
    }
    weekDate = false;
    }
    cal.clear();
    // Set the fields from the min stamp to the max stamp so that
    // the field resolution works in the Calendar.
    for (int stamp = MINIMUM_USER_STAMP; stamp < nextStamp; stamp++) {
    for (int index = 0; index <= maxFieldIndex; index++) {
    if (field[index] == stamp) {
    cal.set(index, field[MAX_FIELD + index]);
    break;
    }
    }
    }
    ...
     }
    }



    1. 先new CalendarBuilder();

    2. 透過parsedDate = calb.establish(calendar).getTime();解析時間;

    3. establish方法內先cal.clear(),再重新構建cal,整個操作沒有加鎖;

    上面幾步就會導致在高並行場景下,執行緒1正在操作一個Calendar,此時執行緒2又來了。執行緒1還沒來得及處理 Calendar 就被執行緒2清空了。

    因此,透過編寫Date工具類,一個執行緒一個SimpleDateFormat,還是有一定道理的。







    ·················END·················

    看完本文有收獲?請轉發分享給更多人

    關註「哪咤編程」,提升Java技能

    點贊和在看就是最大的支持 ❤️