閱讀本文大概需要 3.2 分鐘。
10年積累的成都做網(wǎng)站、成都網(wǎng)站建設(shè)經(jīng)驗(yàn),可以快速應(yīng)對客戶對網(wǎng)站的新想法和需求。提供各種問題對應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認(rèn)識你,你也不認(rèn)識我。但先網(wǎng)站設(shè)計(jì)制作后付款的網(wǎng)站建設(shè)流程,更有長沙縣免費(fèi)網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。前言
日常開發(fā)中,我們經(jīng)常需要使用時(shí)間相關(guān)類,想必大家對 SimpleDateFormat 并不陌生。主要是用它進(jìn)行時(shí)間的 格式化輸出和解析 ,挺方便快捷的,但是 SimpleDateFormat并不是一個(gè)線程安全的類 。在多線程情況下,會(huì)出現(xiàn)異常,想必有經(jīng)驗(yàn)的小伙伴也遇到過。
下面我們就來分析分析SimpleDateFormat為什么不安全?是怎么引發(fā)的?以及多線程下有那些SimpleDateFormat的解決方案?
先看看 《阿里巴巴開發(fā)手冊》 對于SimpleDateFormat是怎么看待的
問題復(fù)現(xiàn)
一般我們在使用SimpleDateFormat的時(shí)候會(huì)把它定義為一個(gè)靜態(tài)變量,避免頻繁創(chuàng)建它們的對象實(shí)例,代碼如下:
打印一下結(jié)果:
是不是感覺沒什么毛?。肯嘈糯蠖鄶?shù)人都是這樣使用的,也包括我。在單線程下自然沒毛病了,但是運(yùn)用到多線程下就有大問題了。
測試下:
控制臺(tái)打印結(jié)果:
你看結(jié)果,發(fā)現(xiàn)了什么?直接崩了,部分線程獲取的時(shí)間不對,部分線程報(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類也不是線程安全的。
來看看 SimpleDateFormat 的 format() 方法的源碼:
注意, 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í)間不對,線程掛死等等。
其實(shí)SimpleDateFormat源碼上作者也給過我們提示:
翻譯過來的意思就是:
日期格式未同步。
建議為每個(gè)線程創(chuàng)建單獨(dú)的格式實(shí)例。
如果多個(gè)線程同時(shí)訪問格式,則必須在外部同步
解決方案
只在需要的時(shí)候創(chuàng)建新實(shí)例,不用static修飾 。
如上代碼,僅在需要用到的地方創(chuàng)建一個(gè)新的實(shí)例,就沒有線程安全問題,不過也加重了創(chuàng)建對象的負(fù)擔(dān), 會(huì)頻繁地創(chuàng)建和銷毀對象,效率較低 。
采用Synchronized方式
簡單粗暴,synchronized往上一套也可以解決線程安全問題,缺點(diǎn)自然就是 并發(fā)量大的時(shí)候會(huì)對性能有影響,線程阻塞 。
ThreadLocal
ThreadLocal可以確保每個(gè)線程都可以得到單獨(dú)的一個(gè)SimpleDateFormat的對象,那么自然也就不存在競爭問題了。
基于JDK1.8的DateTimeFormatter
也是《阿里巴巴開發(fā)手冊》給我們的解決方案,對之前的代碼進(jìn)行改造:
運(yùn)行結(jié)果就不貼了,不會(huì)出現(xiàn)報(bào)錯(cuò)和時(shí)間不準(zhǔn)確的問題。
DateTimeFormatter源碼上作者也加注釋說明了, 他的類是不可變的,并且是線程安全的。
OK,現(xiàn)在是不是可以對你項(xiàng)目里的日期工具類進(jìn)行一波優(yōu)化了呢?
知識擴(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ā)手冊內(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ù)的大值?。?!
所以如果請求太多,隊(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è)插入操作必須等待對應(yīng)的元素被另一個(gè)線程所移除,反之亦然。
因此阻塞隊(duì)列不會(huì)無限拓展而導(dǎo)致OOM。
當(dāng)我們學(xué)習(xí)和理解一些原則的同時(shí),多注重源碼分析!??!
·END·
程序員的成長之路
路雖遠(yuǎn),行則必至
本文原發(fā)于 同名微信公眾號「程序員的成長之路」,回復(fù)「1024」你懂得,給個(gè)贊唄。
微信ID:cxydczzl
往期精彩回顧
程序員接私活的7大平臺(tái)利器
教你一招用 IDE 編程提升效率的騷操作!
大學(xué)期間的副業(yè)賺錢之道
一個(gè)對話讓你明白架構(gòu)師是做什么的?
作為程序員的你,一年看幾本技術(shù)相關(guān)的書
5個(gè)相見恨晚的Linux命令
為啥程序員下班后只關(guān)顯示器從不關(guān)電腦?
送給程序員們的經(jīng)典電子書大禮包
面試時(shí)如何優(yōu)雅地自我介紹?
支撐百萬并發(fā)的數(shù)據(jù)庫架構(gòu)如何設(shè)計(jì)?