多線程環境下使用 DateFormat,DecimalFormat


SimpleDateFormat不是線程安全的(thread safe)。這意味着,下面的代碼在多線程環境下運行結果並非如我們所願 - 有時候,它輸出正確的日期,有時候會輸出錯誤的(例如.Tue Aug 11 00:00:00 CST 48201),有些時候甚至會拋出NumberFormatException!!!(當然,在單線程環境是,這段代碼是完全沒有問題的)

 

打開JDK的源碼,在format方法里,有這樣一段代碼:

 

calendar.setTime(date);

 

其中,calendar是DateFormat的protected字段。這條語句改變了calendar,稍后,calendar還會用到(在subFormat方法里),而這就是引發問題的根源。

 

想象一下,在一個多線程環境下,有兩個線程持有了同一個SimpleDateFormat的實例,分別調用format方法:

 

  1. 線程1調用format方法,改變了calendar這個字段。
  2. 中斷來了。
  3. 線程2開始執行,它也改變了calendar。
  4. 又中斷了。
  5. 線程1回來了,此時,calendar已然不是它所設的值,而是走上了線程2設計的道路。
  6. BANG!!! 稍微花點時間分析一下format的實現,我們便不難發現,用到calendar,唯一的好處,就是在調用subFormat時,少了一個參數,卻帶來了這許多的問題。其實,只要在這里用一個局部變量,一路傳遞下去,所有問題都將迎刃而解。

 

這個問題背后隱藏着一個更為重要的問題:無狀態

 

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

 

所以,寫程序,我們要盡量編寫無狀態方法。

 

我們如何在多線程中的使用DateFormat呢?

1. 同步

最簡單的方法就是在做日期轉換之前,為DateFormat對象加鎖。這種方法使得一次只能讓一個線程訪問DateFormat對象,而其他線程只能等待。

public class DateFormatTest {
    private final DateFormat format = new SimpleDateFormat("yyyyMMdd");
    
    public Date convert(String source) throws ParseException {
        synchronized(format) {
            return format.parse(source);
        }
    }
}

2. 使用ThreadLocal

另外一個方法就是使用ThreadLocal變量去容納DateFormat對象,也就是說每個線程都有一個屬於自己的副本,並無需等待其他線程去釋放它。這種方法會比使用同步塊更高效。

public class DateFormatTest {

    private final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() {

        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyyMMdd");
        }
    };

    public Date convert(String source) throws ParseException {
        return df.get().parse(source);
    }
}

3. Joda-Time

Joda-Time 是一個很棒的開源的 JDK 的日期和日歷 API 的替代品,其 DateTimeFormat 是線程安全而且不變的。

package com.uppower.test;

import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import java.util.Date;

public class DateFormatTest {
    private final DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyyMMdd");
        
    public Date convert(String source){
        DateTime d = fmt.parseDateTime(source);
        returnd.toDate();
    }
}

4.使用臨時變量(不推薦)
 作為一個專業程序員,我們當然知道,相比於共享一個變量的開銷要比每次創建小。創建一個實例來獲取日期格式會比較高效,因為系統不需要多次獲取本地語言和國家。

public class DateFormatTest {
    public Date convert(String source) throws ParseException {
        DateFormat format = new SimpleDateFormat("yyyyMMdd");
        return format.parse(source);
    }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM