點擊關注公眾號,實用技術文章及時了解
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();}}}報錯:

查看源碼中可以看到: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:每個線程都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:每個線程都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);//打印最終結果}}推薦
Java面試題寶典
技術內卷群,一起來學習!!
PS:因為公眾號平台更改了推送規則,如果不想錯過內容,記得讀完點一下「在看」,加個「星標」,這樣每次新文章推送才會第一時間出現在你的訂閱列表里。點「在看」支持我們吧!