閱讀本文大概須要 3.2 分鐘。java
前言 程序員
平常開發中,咱們常常須要使用時間相關類,想必你們對SimpleDateFormat並不陌生。主要是用它進行時間的格式化輸出和解析,挺方便快捷的,可是SimpleDateFormat並非一個線程安全的類。在多線程狀況下,會出現異常,想必有經驗的小夥伴也遇到過。 面試
下面咱們就來分析分析SimpleDateFormat爲何不安全?是怎麼引起的?以及多線程下有那些SimpleDateFormat的解決方案? 數據庫
先看看《阿里巴巴開發手冊》對於SimpleDateFormat是怎麼看待的 編程
問題復現 安全
通常咱們在使用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
關於線程名稱
再次簡單進一步解讀下:
鏈表類型的阻塞隊列,而咱們看其構造函數發現,默認隊列大小是整數的最大值!!!
因此若是請求太多,隊列極可能就耗費內存很是大致使OOM。
可是他們的線程數是固定的,並且通常不會太大,因此不會由於建立過多線程而致使OOM。
其中第最大線程池大小是整數的最大值,所以線程可能不斷建立,乃至到整數的最大值個線程,很容易致使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
往期精彩回顧