線程不安全的SimpleDateFormat

8.5 SimpleDateFormat是線程不安全的

SimpleDateFormat是Java提供的一個格式化和解析日期的工具類,日常開發(fā)中應(yīng)該經(jīng)常會用到,但是由于它是線程不安全的散劫,多線程公用一個SimpleDateFormat實例對日期進行解析或者格式化會導(dǎo)致程序出錯哨鸭,本節(jié)就討論下它為何是線程不安全的蔗蹋,以及如何避免。

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

為了復(fù)現(xiàn)該問題,編寫如下代碼:

 public class TestSimpleDateFormat {
    //(1)創(chuàng)建單例實例
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        //(2)創(chuàng)建多個線程,并啟動
        for (int i = 0; i <10 ; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {//(3)使用單例日期實例解析文本
                        System.out.println(sdf.parse("2017-12-13 15:17:27"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();//(4)啟動線程
        }
    }
}

代碼(1)創(chuàng)建了SimpleDateFormat的一個實例涨岁,代碼(2)創(chuàng)建10個線程拐袜,每個線程都公用同一個sdf對象對文本日期進行解析,多運行幾次就會拋出java.lang.NumberFormatException異常梢薪,加大線程的個數(shù)有利于該問題復(fù)現(xiàn)蹬铺。

問題分析

為了便于分析首先奉上SimpleDateFormat的類圖結(jié)構(gòu):


image.png

可知每個SimpleDateFormat實例里面有一個Calendar對象,從后面會知道其實SimpleDateFormat之所以是線程不安全的就是因為Calendar是線程不安全的秉撇,后者之所以是線程不安全的是因為其中存放日期數(shù)據(jù)的變量都是線程不安全的甜攀,比如里面的fields,time等畜疾。

下面從代碼層面看下parse方法做了什么事情:

    public Date parse(String text, ParsePosition pos)
    {
       
        //(1)解析日期字符串放入CalendarBuilder的實例calb中
        .....

        Date parsedDate;
        try {//(2)使用calb中解析好的日期數(shù)據(jù)設(shè)置calendar
            parsedDate = calb.establish(calendar).getTime();
            ...
        }
       
        catch (IllegalArgumentException e) {
           ...
            return null;
        }

        return parsedDate;
    }
Calendar establish(Calendar cal) {
   ...
   //(3)重置日期對象cal的屬性值
   cal.clear();
   //(4) 使用calb中中屬性設(shè)置cal
   ...
   //(5)返回設(shè)置好的cal對象
   return cal;
}
  • 代碼(1)主要的作用是解析字符串日期并把解析好的數(shù)據(jù)放入了 CalendarBuilder的實例calb中赴邻,CalendarBuilder是一個建造者模式印衔,用來存放后面需要的數(shù)據(jù)啡捶。
  • 代碼(3)重置Calendar對象里面的屬性值,如下代碼:

    public final void clear()
   {
       for (int i = 0; i < fields.length; ) {
           stamp[i] = fields[i] = 0; // UNSET == 0
           isSet[i++] = false;
       }
       areAllFieldsSet = areFieldsSet = false;
       isTimeSet = false;
   }
  • 代碼(4)使用calb中解析好的日期數(shù)據(jù)設(shè)置cal對象
  • 代碼(5) 返回設(shè)置好的cal對象

從上面步驟可知步驟(3)(4)(5)操作不是原子性操作奸焙,當多個線程調(diào)用parse
方法時候比如線程A執(zhí)行了步驟(3)(4)也就是設(shè)置好了cal對象瞎暑,在執(zhí)行步驟(5)前線程B執(zhí)行了步驟(3)清空了cal對象,由于多個線程使用的是一個cal對象与帆,所以線程A執(zhí)行步驟(5)返回的就可能是被線程B清空后的對象了赌,當然也有可能線程B執(zhí)行了步驟(4)被線程B修改后的cal對象。從而導(dǎo)致程序錯誤玄糟。

那么怎么解決那勿她?

  • 第一種方式:每次使用時候new一個SimpleDateFormat的實例,這樣可以保證每個實例使用自己的Calendar實例,但是每次使用都需要new一個對象阵翎,并且使用后由于沒有其它引用逢并,就會需要被回收,開銷會很大郭卫。
  • 第二種方式:究其原因是因為多線程下步驟(3)(4)(5)三個步驟不是一個原子性操作砍聊,那么容易想到的是對其進行同步,讓(3)(4)(5)成為原子操作贰军,可以使用synchronized進行同步玻蝌,具體如下:
public class TestSimpleDateFormat {
    // (1)創(chuàng)建單例實例
    static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static void main(String[] args) {
        // (2)創(chuàng)建多個線程,并啟動
        for (int i = 0; i < 10; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {// (3)使用單例日期實例解析文本
                        synchronized (sdf) {
                            System.out.println(sdf.parse("2017-12-13 15:17:27"));
                        }
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();// (4)啟動線程
        }
    }
}

使用同步意味著多個線程要競爭鎖词疼,在高并發(fā)場景下會導(dǎo)致系統(tǒng)響應(yīng)性能下降俯树。

  • 第三種方式:使用ThreadLocal,這樣每個線程只需要使用一個SimpleDateFormat實例相比第一種方式大大節(jié)省了對象的創(chuàng)建銷毀開銷贰盗,并且不需要對多個線程直接進行同步许饿,使用ThreadLocal方式代碼如下:
public class TestSimpleDateFormat2 {
    // (1)創(chuàng)建threadlocal實例
    static ThreadLocal<DateFormat> safeSdf = new ThreadLocal<DateFormat>(){
        @Override 
        protected SimpleDateFormat initialValue(){
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
    
    public static void main(String[] args) {
        // (2)創(chuàng)建多個線程,并啟動
        for (int i = 0; i < 10; ++i) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    try {// (3)使用單例日期實例解析文本
                            System.out.println(safeSdf.get().parse("2017-12-13 15:17:27"));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.start();// (4)啟動線程
        }
    }
}

代碼(1)創(chuàng)建了一個線程安全的SimpleDateFormat實例童太,步驟(3)在使用的時候首先使用get()方法獲取當前線程下SimpleDateFormat的實例米辐,在第一次調(diào)用ThreadLocal的get()方法適合會觸發(fā)其initialValue方法用來創(chuàng)建當前線程所需要的SimpleDateFormat對象胸完。

總結(jié)

本節(jié)通過簡單介紹SimpleDateFormat的原理說明了SimpleDateFormat是線程不安全的,應(yīng)該避免多線程下使用SimpleDateFormat的單個實例翘贮,多線程下使用時候最好使用ThreadLocal對象赊窥。更多并發(fā)編程中需要注意的情景以及解決方法敬請期待 Java中高并發(fā)編程必備基礎(chǔ)之并發(fā)包源碼剖析 一書出版

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市狸页,隨后出現(xiàn)的幾起案子锨能,更是在濱河造成了極大的恐慌,老刑警劉巖芍耘,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件址遇,死亡現(xiàn)場離奇詭異,居然都是意外死亡斋竞,警方通過查閱死者的電腦和手機倔约,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來坝初,“玉大人浸剩,你說我怎么就攤上這事■郏” “怎么了绢要?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長拗小。 經(jīng)常有香客問我重罪,道長,這世上最難降的妖魔是什么哀九? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任剿配,我火速辦了婚禮,結(jié)果婚禮上勾栗,老公的妹妹穿的比我還像新娘惨篱。我一直安慰自己,他們只是感情好围俘,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布砸讳。 她就那樣靜靜地躺著,像睡著了一般界牡。 火紅的嫁衣襯著肌膚如雪簿寂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天宿亡,我揣著相機與錄音常遂,去河邊找鬼。 笑死挽荠,一個胖子當著我的面吹牛克胳,可吹牛的內(nèi)容都是我干的平绩。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼漠另,長吁一口氣:“原來是場噩夢啊……” “哼捏雌!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起笆搓,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤性湿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后满败,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肤频,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年算墨,在試婚紗的時候發(fā)現(xiàn)自己被綠了宵荒。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡米同,死狀恐怖骇扇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情面粮,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布继低,位于F島的核電站熬苍,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏袁翁。R本人自食惡果不足惜柴底,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望粱胜。 院中可真熱鬧柄驻,春花似錦、人聲如沸焙压。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽涯曲。三九已至野哭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間幻件,已是汗流浹背拨黔。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留绰沥,地道東北人篱蝇。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓贺待,卻偏偏與公主長得像,于是被迫代替她去往敵國和親零截。 傳聞我的和親對象是個殘疾皇子狠持,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

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