還在使用SimpleDateFormat?

閱讀本文大概需要 3.2 分鐘。

前言

日常開發(fā)中,我們經(jīng)常需要使用時(shí)間相關(guān)類,想必大家對(duì)SimpleDateFormat并不陌生。主要是用它進(jìn)行時(shí)間的格式化輸出和解析,挺方便快捷的,但是SimpleDateFormat并不是一個(gè)線程安全的類。在多線程情況下,會(huì)出現(xiàn)異常,想必有經(jīng)驗(yàn)的小伙伴也遇到過。

下面我們就來分析分析SimpleDateFormat為什么不安全?是怎么引發(fā)的?以及多線程下有那些SimpleDateFormat的解決方案?

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

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

一般我們?cè)谑褂肧impleDateFormat的時(shí)候會(huì)把它定義為一個(gè)靜態(tài)變量,避免頻繁創(chuàng)建它們的對(duì)象實(shí)例,代碼如下:

打印一下結(jié)果:

是不是感覺沒什么毛?。肯嘈糯蠖鄶?shù)人都是這樣使用的,也包括我。在單線程下自然沒毛病了,但是運(yùn)用到多線程下就有大問題了。

測(cè)試下:

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

你看結(jié)果,發(fā)現(xiàn)了什么?直接崩了,部分線程獲取的時(shí)間不對(duì),部分線程報(bào)java.lang.NumberFormatException:multiple points錯(cuò),線程直接掛死了。還有部分線程報(bào)empty String錯(cuò),值有問題。

多線程不安全原因

因?yàn)槲覀儼裇impleDateFormat定義為靜態(tài)變量,那么多線程下SimpleDateFormat的實(shí)例就會(huì)被多個(gè)線程共享,B線程會(huì)讀取到A線程的時(shí)間,就會(huì)出現(xiàn)時(shí)間差異和其它各種問題。SimpleDateFormat和它繼承的DateFormat類也不是線程安全的。

來看看SimpleDateFormatformat()方法的源碼:

注意, calendar.setTime(date),SimpleDateFormat的format方法實(shí)際操作的就是Calendar。

因?yàn)槲覀兟暶鱏impleDateFormat為static變量,那么它的Calendar變量也就是一個(gè)共享變量,可以被多個(gè)線程訪問。

假設(shè)線程A執(zhí)行完calendar.setTime(date),把時(shí)間設(shè)置成2019-01-02,這時(shí)候被掛起,線程B獲得CPU執(zhí)行權(quán)。線程B也執(zhí)行到了calendar.setTime(date),把時(shí)間設(shè)置為2019-01-03。線程掛起,線程A繼續(xù)走,calendar還會(huì)被繼續(xù)使用(subFormat方法),而這時(shí)calendar用的是線程B設(shè)置的值了,而這就是引發(fā)問題的根源,出現(xiàn)時(shí)間不對(duì),線程掛死等等。

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

翻譯過來的意思就是:

日期格式未同步。

建議為每個(gè)線程創(chuàng)建單獨(dú)的格式實(shí)例。

如果多個(gè)線程同時(shí)訪問格式,則必須在外部同步

解決方案

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

如上代碼,僅在需要用到的地方創(chuàng)建一個(gè)新的實(shí)例,就沒有線程安全問題,不過也加重了創(chuàng)建對(duì)象的負(fù)擔(dān),會(huì)頻繁地創(chuàng)建和銷毀對(duì)象,效率較低

采用Synchronized方式

簡單粗暴,synchronized往上一套也可以解決線程安全問題,缺點(diǎn)自然就是并發(fā)量大的時(shí)候會(huì)對(duì)性能有影響,線程阻塞。

ThreadLocal

ThreadLocal可以確保每個(gè)線程都可以得到單獨(dú)的一個(gè)SimpleDateFormat的對(duì)象,那么自然也就不存在競(jìng)爭(zhēng)問題了。

基于JDK1.8的DateTimeFormatter

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

運(yùn)行結(jié)果就不貼了,不會(huì)出現(xiàn)報(bào)錯(cuò)和時(shí)間不準(zhǔn)確的問題。

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

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

知識(shí)擴(kuò)展

在上述代碼中,我們通過創(chuàng)建一個(gè)線程池,來實(shí)現(xiàn)多線程循環(huán)打印日期的操作,但是我們創(chuàng)建方式你有沒有留意。

ExecutorService executorService = Executors.newFixedThreadPool(100);

當(dāng)你IDEA安裝了阿里巴巴的代碼規(guī)范檢查插件時(shí),使用Executors來創(chuàng)建線程池的話,會(huì)出現(xiàn)提示讓你手動(dòng)創(chuàng)建線程池。

因此,我們可以將創(chuàng)建線程池的代碼改成:

ExecutorService executorService = new ThreadPoolExecutor(100, 100,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());

但是又會(huì)有提示,建議要為線程池中的線程設(shè)置名稱:

改造之后的代碼為:

ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("thread-call-runner-%d").build();

ExecutorService executorService = new ThreadPoolExecutor(100, 100,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());

這里會(huì)有個(gè)問題,ThreadFactoryBuilder()在JDK1.8及之后被去除了,所以如果你的JDK低于1.8即可使用該方法,等于或高于1.8可采取其他方式設(shè)置線程名稱,也可用其他方式手動(dòng)創(chuàng)建線程池。

為什么要這樣做

我們參考阿里巴巴的Java開發(fā)手冊(cè)內(nèi)容:

關(guān)于Executors

關(guān)于線程名稱

再次簡單進(jìn)一步解讀下:

newFixedThreadPool和newSingleThreadExecutor 由于最后一個(gè)參數(shù)即工作隊(duì)列是

鏈表類型的阻塞隊(duì)列,而我們看其構(gòu)造函數(shù)發(fā)現(xiàn),默認(rèn)隊(duì)列大小是整數(shù)的最大值?。。?/p>

所以如果請(qǐng)求太多,隊(duì)列很可能就耗費(fèi)內(nèi)存非常大導(dǎo)致OOM。

但是他們的線程數(shù)是固定的,而且一般不會(huì)太大,所以不會(huì)因?yàn)閯?chuàng)建過多線程而導(dǎo)致OOM。

再來看下newCachedThreadPool和newScheduledThreadPool

其中第最大線程池大小是整數(shù)的最大值,因此線程可能不斷創(chuàng)建,乃至到整數(shù)的最大值個(gè)線程,很容易導(dǎo)致OOM。其中工作隊(duì)列使用的是 SynchronousQueue,源碼頭部的注釋中有說明(截取的部分)。

A {@linkplain BlockingQueue blocking queue} in which each insert operation must wait for a corresponding remove operation by another thread, and vice versa.

該類型的阻塞隊(duì)列每一個(gè)插入操作必須等待對(duì)應(yīng)的元素被另一個(gè)線程所移除,反之亦然。

因此阻塞隊(duì)列不會(huì)無限拓展而導(dǎo)致OOM。

當(dāng)我們學(xué)習(xí)和理解一些原則的同時(shí),多注重源碼分析?。?!

·END·

程序員的成長之路

路雖遠(yuǎn),行則必至

本文原發(fā)于 同名微信公眾號(hào)「程序員的成長之路」,回復(fù)「1024」你懂得,給個(gè)贊唄。

微信ID:cxydczzl

往期精彩回顧

程序員接私活的7大平臺(tái)利器

教你一招用 IDE 編程提升效率的騷操作!

大學(xué)期間的副業(yè)賺錢之道

一個(gè)對(duì)話讓你明白架構(gòu)師是做什么的?

作為程序員的你,一年看幾本技術(shù)相關(guān)的書

5個(gè)相見恨晚的Linux命令

為啥程序員下班后只關(guān)顯示器從不關(guān)電腦?

送給程序員們的經(jīng)典電子書大禮包

面試時(shí)如何優(yōu)雅地自我介紹?

支撐百萬并發(fā)的數(shù)據(jù)庫架構(gòu)如何設(shè)計(jì)?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容