您好,登錄后才能下訂單哦!
閱讀本文大概需要 3.2 分鐘。
前言
日常開發中,我們經常需要使用時間相關類,想必大家對
SimpleDateFormat
并不陌生。主要是用它進行時間的
格式化輸出和解析
,挺方便快捷的,但是
SimpleDateFormat并不是一個線程安全的類
。在多線程情況下,會出現異常,想必有經驗的小伙伴也遇到過。
下面我們就來分析分析SimpleDateFormat為什么不安全?是怎么引發的?以及多線程下有那些SimpleDateFormat的解決方案?
先看看
《阿里巴巴開發手冊》
對于SimpleDateFormat是怎么看待的
cdn.xitu.io/2019/3/21/1699de9099afa9f4?w=728&h=328&f=jpeg&s=38443">
問題復現
一般我們在使用SimpleDateFormat的時候會把它定義為一個靜態變量,避免頻繁創建它們的對象實例,代碼如下:
打印一下結果:
是不是感覺沒什么毛病?相信大多數人都是這樣使用的,也包括我。在單線程下自然沒毛病了,但是運用到多線程下就有大問題了。
測試下:
控制臺打印結果:
你看結果,發現了什么?直接崩了,部分線程獲取的時間不對,部分線程報
java.lang.NumberFormatException:multiple points
錯,線程直接掛死了。還有部分線程報
empty String
錯,值有問題。
多線程不安全原因
因為我們把SimpleDateFormat定義為
靜態變量
,那么多線程下SimpleDateFormat的實例就會
被多個線程共享
,B線程會讀取到A線程的時間,就會出現時間差異和其它各種問題。SimpleDateFormat和它繼承的DateFormat類也不是線程安全的。
來看看
SimpleDateFormat
的
format()
方法的源碼:
注意, calendar.setTime(date),SimpleDateFormat的format方法實際操作的就是
Calendar
。
因為我們聲明SimpleDateFormat為static變量,那么它的Calendar變量也就是一個共享變量,
可以被多個線程訪問
。
假設線程A執行完calendar.setTime(date),把時間設置成2019-01-02,這時候被掛起,線程B獲得CPU執行權。線程B也執行到了calendar.setTime(date),把時間設置為2019-01-03。線程掛起,線程A繼續走,calendar還會被繼續使用(subFormat方法),而這時calendar用的是線程B設置的值了,而這就是引發問題的根源,出現時間不對,線程掛死等等。
其實SimpleDateFormat源碼上作者也給過我們提示:
翻譯過來的意思就是:
日期格式未同步。
建議為每個線程創建單獨的格式實例。
如果多個線程同時訪問格式,則必須在外部同步
解決方案
只在需要的時候創建新實例,不用static修飾
。
如上代碼,僅在需要用到的地方創建一個新的實例,就沒有線程安全問題,不過也加重了創建對象的負擔,
會頻繁地創建和銷毀對象,效率較低
。
采用Synchronized方式
簡單粗暴,synchronized往上一套也可以解決線程安全問題,缺點自然就是
并發量大的時候會對性能有影響,線程阻塞
。
ThreadLocal
ThreadLocal可以確保每個線程都可以得到單獨的一個SimpleDateFormat的對象,那么自然也就不存在競爭問題了。
基于JDK1.8的DateTimeFormatter
也是《阿里巴巴開發手冊》給我們的解決方案,對之前的代碼進行改造:
運行結果就不貼了,不會出現報錯和時間不準確的問題。
DateTimeFormatter源碼上作者也加注釋說明了, 他的類是不可變的,并且是線程安全的。
OK,現在是不是可以對你項目里的日期工具類進行一波優化了呢?
知識擴展
在上述代碼中,我們通過創建一個線程池,來實現多線程循環打印日期的操作,但是我們創建方式你有沒有留意。
ExecutorService executorService = Executors.newFixedThreadPool(100);
當你IDEA安裝了阿里巴巴的代碼規范檢查插件時,使用Executors來創建線程池的話,會出現提示讓你手動創建線程池。
因此,我們可以將創建線程池的代碼改成:
ExecutorService executorService = new ThreadPoolExecutor(100, 100,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
但是又會有提示,建議要為線程池中的線程設置名稱:
改造之后的代碼為:
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("thread-call-runner-%d").build(); ExecutorService executorService = new ThreadPoolExecutor(100, 100,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
這里會有個問題,
ThreadFactoryBuilder()
在JDK1.8及之后被去除了,所以如果你的JDK低于1.8即可使用該方法,等于或高于1.8可采取其他方式設置線程名稱,也可用其他方式手動創建線程池。
為什么要這樣做
我們參考阿里巴巴的Java開發手冊內容:
關于Executors
關于線程名稱
再次簡單進一步解讀下:
newFixedThreadPool和newSingleThreadExecutor 由于最后一個參數即工作隊列是
鏈表類型的阻塞隊列,而我們看其構造函數發現,默認隊列大小是整數的最大值!!!
所以如果請求太多,隊列很可能就耗費內存非常大導致OOM。
但是他們的線程數是固定的,而且一般不會太大,所以不會因為創建過多線程而導致OOM。
再來看下newCachedThreadPool和newScheduledThreadPool
其中第最大線程池大小是整數的最大值,因此線程可能不斷創建,乃至到整數的最大值個線程,很容易導致OOM。其中工作隊列使用的是 SynchronousQueue<E>,源碼頭部的注釋中有說明(截取的部分)。
A {@linkplain BlockingQueue blocking queue} in which each insert operation must wait for a corresponding remove operation by another thread, and vice versa.
該類型的阻塞隊列每一個插入操作必須等待對應的元素被另一個線程所移除,反之亦然。
因此阻塞隊列不會無限拓展而導致OOM。
當我們學習和理解一些原則的同時,多注重源碼分析!!!
·END·
程序員的成長之路
路雖遠,行則必至
本文原發于 同名微信公眾號「程序員的成長之路」,回復「1024」你懂得,給個贊唄。
微信ID:cxydczzl
往期精彩回顧
程序員接私活的7大平臺利器
教你一招用 IDE 編程提升效率的騷操作!
大學期間的副業賺錢之道
一個對話讓你明白架構師是做什么的?
作為程序員的你,一年看幾本技術相關的書
5個相見恨晚的Linux命令
為啥程序員下班后只關顯示器從不關電腦?
送給程序員們的經典電子書大禮包
面試時如何優雅地自我介紹?
支撐百萬并發的數據庫架構如何設計?
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。