close

點擊關注公眾號,實用技術文章及時了解


SimpleDateFormat.parse() 方法的線程安全問題
錯誤示例
非線程安全原因分析
解決方法
SimpleDateFormat.format() 方法的線程安全問題
錯誤示例
非線程安全原因分析
解決方法

SimpleDateFormat在多線程環境下存在線程安全問題。

1 SimpleDateFormat.parse() 方法的線程安全問題1.1 錯誤示例

錯誤使用SimpleDateFormat.parse()的代碼如下:

importjava.text.SimpleDateFormat;publicclassSimpleDateFormatTest{privatestaticfinalSimpleDateFormatSIMPLE_DATE_FORMAT=newSimpleDateFormat("yyyy-MM-ddHH:mm:ss");publicstaticvoidmain(String[]args){/***SimpleDateFormat線程不安全,沒有保證線程安全(沒有加鎖)的情況下,禁止使用全局SimpleDateFormat,否則報錯NumberFormatException**privatestaticfinalSimpleDateFormatdateFormat=newSimpleDateFormat("yyyy-MM-ddHH:mm:ss");*/for(inti=0;i<20;++i){Threadthread=newThread(()->{try{//錯誤寫法會導致線程安全問題System.out.println(Thread.currentThread().getName()+"--"+SIMPLE_DATE_FORMAT.parse("2020-06-0111:35:00"));}catch(Exceptione){e.printStackTrace();}},"Thread-"+i);thread.start();}}}

報錯:

1.2 非線程安全原因分析

查看源碼中可以看到:SimpleDateFormat繼承DateFormat類,SimpleDateFormat轉換日期是通過繼承自DateFormat類的Calendar對象來操作的,Calendar對象會被用來進行日期-時間計算,既被用於format方法也被用於parse方法。

SimpleDateFormat 的 parse(String source) 方法 會調用繼承自父類的 DateFormat 的 parse(String source) 方法

DateFormat 的 parse(String source) 方法會調用SimpleDateFormat中重寫的 parse(String text, ParsePosition pos) 方法,該方法中有個地方需要關注

SimpleDateFormat 中重寫的 parse(String text, ParsePosition pos) 方法中調用了 establish(calendar) 這個方法:

該方法中調用了 Calendar 的 clear() 方法

可以發現整個過程中Calendar對象它並不是線程安全的,如果,a線程將calendar清空了,calendar 就沒有新值了,恰好此時b線程剛好進入到parse方法用到了calendar對象,那就會產生線程安全問題了!

正常情況下:

非線程安全的流程:

1.3 解決方法

方法1:每個線程都new一個SimpleDateFormat

importjava.text.SimpleDateFormat;publicclassSimpleDateFormatTest{publicstaticvoidmain(String[]args){for(inti=0;i<20;++i){Threadthread=newThread(()->{try{//每個線程都new一個SimpleDateFormatsimpleDateFormat=newSimpleDateFormat("yyyy-MM-ddHH:mm:ss");System.out.println(Thread.currentThread().getName()+"--"+simpleDateFormat.parse("2020-06-0111:35:00"));}catch(Exceptione){e.printStackTrace();}},"Thread-"+i);thread.start();}}}

方式2:synchronized等方式加鎖

publicclassSimpleDateFormatTest{privatestaticfinalSimpleDateFormatSIMPLE_DATE_FORMAT=newSimpleDateFormat("yyyy-MM-ddHH:mm:ss");publicstaticvoidmain(String[]args){for(inti=0;i<20;++i){Threadthread=newThread(()->{try{synchronized(SIMPLE_DATE_FORMAT){System.out.println(Thread.currentThread().getName()+"--"+SIMPLE_DATE_FORMAT.parse("2020-06-0111:35:00"));}}catch(Exceptione){e.printStackTrace();}},"Thread-"+i);thread.start();}}}

方式3:使用ThreadLocal 為每個線程創建一個獨立變量

importjava.text.DateFormat;importjava.text.SimpleDateFormat;publicclassSimpleDateFormatTest{privatestaticfinalThreadLocal<DateFormat>SAFE_SIMPLE_DATE_FORMAT=ThreadLocal.withInitial(()->newSimpleDateFormat("yyyy-MM-ddHH:mm:ss"));publicstaticvoidmain(String[]args){for(inti=0;i<20;++i){Threadthread=newThread(()->{try{System.out.println(Thread.currentThread().getName()+"--"+SAFE_SIMPLE_DATE_FORMAT.get().parse("2020-06-0111:35:00"));}catch(Exceptione){e.printStackTrace();}},"Thread-"+i);thread.start();}}}

ThreadLocal的詳細使用細節見:

https://blog.csdn.net/QiuHaoqian/article/details/117077792

2 SimpleDateFormat.format() 方法的線程安全問題2.1 錯誤示例importjava.text.SimpleDateFormat;importjava.util.Date;importjava.util.concurrent.LinkedBlockingQueue;importjava.util.concurrent.ThreadPoolExecutor;importjava.util.concurrent.TimeUnit;publicclassSimpleDateFormatTest{//時間格式化對象privatestaticSimpleDateFormatsimpleDateFormat=newSimpleDateFormat("mm:ss");publicstaticvoidmain(String[]args)throwsInterruptedException{//創建線程池執行任務ThreadPoolExecutorthreadPool=newThreadPoolExecutor(10,10,60,TimeUnit.SECONDS,newLinkedBlockingQueue<>(1000));for(inti=0;i<1000;i++){intfinalI=i;//執行任務threadPool.execute(newRunnable(){@Overridepublicvoidrun(){Datedate=newDate(finalI*1000);//得到時間對象formatAndPrint(date);//執行時間格式化}});}threadPool.shutdown();//線程池執行完任務之後關閉}/***格式化並打印時間*/privatestaticvoidformatAndPrint(Datedate){Stringresult=simpleDateFormat.format(date);//執行格式化System.out.println("時間:"+result);//打印最終結果}}

從上述結果可以看出,程序的打印結果竟然有重複內容的,正確的情況應該是沒有重複的時間才對。

2.2 非線程安全原因分析

為了找到問題所在,查看 SimpleDateFormat 中 format 方法的源碼來排查一下問題,format 源碼如下:

從上述源碼可以看出,在執行 SimpleDateFormat.format() 方法時,會使用 calendar.setTime() 方法將輸入的時間進行轉換,那麼我們想想一下這樣的場景:

線程 1 執行了 calendar.setTime(date) 方法,將用戶輸入的時間轉換成了後面格式化時所需要的時間;
線程 1 暫停執行,線程 2 得到 CPU 時間片開始執行;
線程 2 執行了 calendar.setTime(date) 方法,對時間進行了修改;
線程 2 暫停執行,線程 1 得出 CPU 時間片繼續執行,因為線程 1 和線程 2 使用的是同一對象,而時間已經被線程 2 修改了,所以此時當線程 1 繼續執行的時候就會出現線程安全的問題了。

正常的情況下,程序的執行是這樣的:

非線程安全的執行流程是這樣的:

2.3 解決方法

同樣有三種解決方法

方法1:每個線程都new一個SimpleDateFormat

publicclassSimpleDateFormatTest{publicstaticvoidmain(String[]args)throwsInterruptedException{//創建線程池執行任務ThreadPoolExecutorthreadPool=newThreadPoolExecutor(10,10,60,TimeUnit.SECONDS,newLinkedBlockingQueue<>(1000));for(inti=0;i<1000;i++){intfinalI=i;//執行任務threadPool.execute(newRunnable(){@Overridepublicvoidrun(){//得到時間對象Datedate=newDate(finalI*1000);//執行時間格式化formatAndPrint(date);}});}//線程池執行完任務之後關閉threadPool.shutdown();}/***格式化並打印時間*/privatestaticvoidformatAndPrint(Datedate){Stringresult=newSimpleDateFormat("mm:ss").format(date);//執行格式化System.out.println("時間:"+result);//打印最終結果}}

方式2:synchronized等方式加鎖

所有的線程必須排隊執行某些業務才行,這樣無形中就降低了程序的運行效率了

publicclassSimpleDateFormatTest{//時間格式化對象privatestaticSimpleDateFormatsimpleDateFormat=newSimpleDateFormat("mm:ss");publicstaticvoidmain(String[]args)throwsInterruptedException{//創建線程池執行任務ThreadPoolExecutorthreadPool=newThreadPoolExecutor(10,10,60,TimeUnit.SECONDS,newLinkedBlockingQueue<>(1000));for(inti=0;i<1000;i++){intfinalI=i;//執行任務threadPool.execute(newRunnable(){@Overridepublicvoidrun(){Datedate=newDate(finalI*1000);//得到時間對象formatAndPrint(date);//執行時間格式化}});}//線程池執行完任務之後關閉threadPool.shutdown();}/***格式化並打印時間*/privatestaticvoidformatAndPrint(Datedate){//執行格式化Stringresult=null;//加鎖synchronized(SimpleDateFormatTest.class){result=simpleDateFormat.format(date);}//打印最終結果System.out.println("時間:"+result);}}

方式3:使用ThreadLocal 為每個線程創建一個獨立變量

publicclassSimpleDateFormatTest{//創建ThreadLocal並設置默認值privatestaticThreadLocal<SimpleDateFormat>dateFormatThreadLocal=ThreadLocal.withInitial(()->newSimpleDateFormat("mm:ss"));publicstaticvoidmain(String[]args){//創建線程池執行任務ThreadPoolExecutorthreadPool=newThreadPoolExecutor(10,10,60,TimeUnit.SECONDS,newLinkedBlockingQueue<>(1000));//執行任務for(inti=0;i<1000;i++){intfinalI=i;//執行任務threadPool.execute(()->{Datedate=newDate(finalI*1000);//得到時間對象formatAndPrint(date);//執行時間格式化});}threadPool.shutdown();//線程池執行完任務之後關閉}/***格式化並打印時間*/privatestaticvoidformatAndPrint(Datedate){Stringresult=dateFormatThreadLocal.get().format(date);//執行格式化System.out.println("時間:"+result);//打印最終結果}}
來源:blog.csdn.net/QiuHaoqian/article/details/116594422

推薦

Java面試題寶典

技術內卷群,一起來學習!!

PS:因為公眾號平台更改了推送規則,如果不想錯過內容,記得讀完點一下「在看」,加個「星標」,這樣每次新文章推送才會第一時間出現在你的訂閱列表里。點「在看」支持我們吧!

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()