為什么不建議使用 SimpleDateFormat

轉(zhuǎn)載自 還在使用SimpleDateFormat瑞驱?你的項目崩沒?

還在使用SimpleDateFormat?你的項目崩沒拍顷?

論SimpleDateFormat線程安全問題及解決方案

日常開發(fā)中,我們經(jīng)常需要使用時間相關(guān)類督弓,說到時間相關(guān)類营曼,想必大家對SimpleDateFormat并不陌生。主要是用它進(jìn)行時間的格式化輸出和解析愚隧,挺方便快捷的蒂阱,但是SimpleDateFormat并不是一個線程安全的類。在多線程情況下狂塘,會出現(xiàn)異常录煤,想必有經(jīng)驗的小伙伴也遇到過。下面我們就來分析分析SimpleDateFormat為什么不安全荞胡?是怎么引發(fā)的妈踊?以及多線程下有那些SimpleDateFormat的解決方案?

先看看《阿里巴巴開發(fā)手冊》對于SimpleDateFormat是怎么看待的:

img

附《阿里巴巴Java開發(fā)手冊》v1.4.0(詳盡版)下載鏈接:https://yfzhou.oss-cn-beijing.aliyuncs.com/blog/img/《阿里巴巴開發(fā)手冊》v 1.4.0.pdf

問題場景復(fù)現(xiàn)

一般我們使用SimpleDateFormat的時候會把它定義為一個靜態(tài)變量泪漂,避免頻繁創(chuàng)建它的對象實例响委,如下代碼:

public class SimpleDateFormatTest {

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

    public static void main(String[] args) throws InterruptedException, ParseException {

        System.out.println(sdf.format(new Date()));
        
    }
}

是不是感覺沒什么毛病窖梁?單線程下自然沒毛病了赘风,都是運用到多線程下就有大問題了。
測試下:

public static void main(String[] args) throws InterruptedException, ParseException {

    ExecutorService service = Executors.newFixedThreadPool(100);

    for (int i = 0; i < 20; i++) {
        service.execute(() -> {
            for (int j = 0; j < 10; j++) {
                try {
                    System.out.println(parse("2018-01-02 09:45:59"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        });
    }
    // 等待上述的線程執(zhí)行完
    service.shutdown();
    service.awaitTermination(1, TimeUnit.DAYS);
}

控制臺打印結(jié)果:

img

你看這不崩了纵刘?部分線程獲取的時間不對邀窃,部分線程直接報java.lang.NumberFormatException: multiple points錯,線程直接掛死了假哎。

多線程不安全原因

因為我們吧SimpleDateFormat定義為靜態(tài)變量瞬捕,那么多線程下SimpleDateFormat的實例就會被多個線程共享,B線程會讀取到A線程的時間舵抹,就會出現(xiàn)時間差異和其它各種問題肪虎。SimpleDateFormat和它繼承的DateFormat類也不是線程安全的

來看看SimpleDateFormat的format()方法的源碼

// 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);,SimpleDateFormat的format方法實際操作的就是Calendar惧蛹。

因為我們聲明SimpleDateFormat為static變量扇救,那么它的Calendar變量也就是一個共享變量,可以被多個線程訪問香嗓。

假設(shè)線程A執(zhí)行完calendar.setTime(date)迅腔,把時間設(shè)置成2019-01-02,這時候被掛起靠娱,線程B獲得CPU執(zhí)行權(quán)沧烈。線程B也執(zhí)行到了calendar.setTime(date),把時間設(shè)置為2019-01-03像云。線程掛起锌雀,線程A繼續(xù)走蚂夕,calendar還會被繼續(xù)使用(subFormat方法),而這時calendar用的是線程B設(shè)置的值了腋逆,而這就是引發(fā)問題的根源双抽,出現(xiàn)時間不對,線程掛死等等闲礼。

其實SimpleDateFormat源碼上作者也給過我們提示:

* 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.

意思就是

日期格式不同步牍汹。
建議為每個線程創(chuàng)建單獨的格式實例。
如果多個線程同時訪問一種格式柬泽,則必須在外部同步該格式慎菲。

解決方案

只在需要的時候創(chuàng)建新實例,不用static修飾

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

如上代碼锨并,僅在需要用到的地方創(chuàng)建一個新的實例露该,就沒有線程安全問題,不過也加重了創(chuàng)建對象的負(fù)擔(dān)第煮,會頻繁地創(chuàng)建和銷毀對象解幼,效率較低。

synchronized大法好

private static final 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);
    }
}

簡單粗暴包警,synchronized往上一套也可以解決線程安全問題撵摆,缺點自然就是并發(fā)量大的時候會對性能有影響,線程阻塞害晦。

ThreadLocal

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可以確保每個線程都可以得到單獨的一個SimpleDateFormat的對象特铝,那么自然也就不存在競爭問題了。

基于JDK1.8的DateTimeFormatter

也是《阿里巴巴開發(fā)手冊》給我們的解決方案壹瘟,對之前的代碼進(jìn)行改造:

public class SimpleDateFormatTest {

    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static String formatDate2(LocalDateTime date) {
        return formatter.format(date);
    }

    public static LocalDateTime parse2(String dateNow) {
        return LocalDateTime.parse(dateNow, formatter);
    }

    public static void main(String[] args) throws InterruptedException, ParseException {

        ExecutorService service = Executors.newFixedThreadPool(100);

        // 20個線程
        for (int i = 0; i < 20; i++) {
            service.execute(() -> {
                for (int j = 0; j < 10; j++) {
                    try {
                        System.out.println(parse2(formatDate2(LocalDateTime.now())));
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }
        // 等待上述的線程執(zhí)行完
        service.shutdown();
        service.awaitTermination(1, TimeUnit.DAYS);


    }
}

運行結(jié)果就不貼了鲫剿,不會出現(xiàn)報錯和時間不準(zhǔn)確的問題。

DateTimeFormatter源碼上作者也加注釋說明了稻轨,他的類是不可變的灵莲,并且是線程安全的。

* This class is immutable and thread-safe.

ok殴俱,現(xiàn)在是不是可以對你項目里的日期工具類進(jìn)行一波優(yōu)化了呢政冻?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市粱挡,隨后出現(xiàn)的幾起案子赠幕,更是在濱河造成了極大的恐慌俄精,老刑警劉巖询筏,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異竖慧,居然都是意外死亡嫌套,警方通過查閱死者的電腦和手機(jī)逆屡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來踱讨,“玉大人魏蔗,你說我怎么就攤上這事”陨福” “怎么了莺治?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長帚稠。 經(jīng)常有香客問我谣旁,道長,這世上最難降的妖魔是什么滋早? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任榄审,我火速辦了婚禮,結(jié)果婚禮上杆麸,老公的妹妹穿的比我還像新娘搁进。我一直安慰自己,他們只是感情好昔头,可當(dāng)我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布饼问。 她就那樣靜靜地躺著,像睡著了一般揭斧。 火紅的嫁衣襯著肌膚如雪匆瓜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天未蝌,我揣著相機(jī)與錄音驮吱,去河邊找鬼。 笑死萧吠,一個胖子當(dāng)著我的面吹牛左冬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播纸型,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼拇砰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了狰腌?” 一聲冷哼從身側(cè)響起除破,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎琼腔,沒想到半個月后瑰枫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年光坝,在試婚紗的時候發(fā)現(xiàn)自己被綠了尸诽。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡盯另,死狀恐怖性含,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情鸳惯,我是刑警寧澤商蕴,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站芝发,受9級特大地震影響究恤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜后德,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一部宿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧瓢湃,春花似錦理张、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至落蝙,卻和暖如春织狐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背筏勒。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工移迫, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人管行。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓厨埋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親捐顷。 傳聞我的和親對象是個殘疾皇子荡陷,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容