Java多執行緒中static變數的使用 SimpleDateFormat時間格式化存線上程安全問題
兩篇文章
Java多執行緒中static變數的使用
(轉自:http://blog.csdn.net/yy304935305/article/details/52456771)
&&
SimpleDateFormat時間格式化存線上程安全問題
(https://www.cnblogs.com/zhuimengdeyuanyuan/archive/2017/10/25/7728009.html)
Java多執行緒中static變數的使用
有時候,對於在多執行緒中使用static變數有沒有衝突,是否存在安全問題不能十分的確定。在使用過程中有點含糊,總想找點時間好好追究一下,可總因開發專案時間的緊迫而擱淺。我想,沒有做進一步的研究而拿專案繁忙說事,這是自己的藉口吧!
魯迅先生曾說過:“時間就像海綿裡的水,只要願擠,總還是有的”。不管腫(怎)麼說,這事還是要做的啊。如果越往後推,可能造成的潛在影響更大。這始終是個隱患,不能不除。
不是痛定思痛,而是認識到事情的重要性,就要開始行動了... ...
以上是個人的閒言碎語,不足而看。下面,我們就少言幾句進入到技術領域吧!
執行緒,是我們專案中繞不過的重點領域。提到執行緒,就常會聽到執行緒安全的術語。那什麼是執行緒安全呢?通俗點說,就是執行緒訪問時不產生資源衝突。其實,這是一個有點難以定義的概念,不是很容易讓人一聽就懂的概念。“一個類可以被多個執行緒安全呼叫就是執行緒安全的”《Java程式設計併發實踐》。
來說說靜態變數、例項變數、區域性變數在多執行緒下的安全問題吧!
(一)靜態變數:執行緒非安全
1、靜態變數:使用static關鍵字定義的變數。static可以修飾變數和方法,也有static靜態程式碼塊。被static修飾的成員變數和成員方法獨立於該類的任何物件。也就是說,它不依賴類特定的例項,被類的所有例項共享
用public修飾的static成員變數和成員方法本質是變數和全域性方法,當宣告它的類的物件時,不生成static變數的副本,而是類的所有例項共享同一個static變數。
2、靜態變數使用的場景:
(1)物件間共享值時
(2)方便訪問變數時
3、靜態方法使用注意事項:
(1)不能在靜態方法內使用非靜態變數,即不能直接訪問所屬類的例項變數;
(2)不能在靜態方法內直接呼叫非靜態方法;
(3)靜態方法中不能使用this和super關鍵字;
4、驗證靜態變數的執行緒安全性:
(1)從程式執行的圖中我們可以看出,執行結果中有錯誤資料,證明了靜態變數是存在資源衝突問題的。
(2)程式執行結果圖:
5、結論:靜態變數也稱為類變數,屬於類物件所有,位於方法區,為所有物件共享,共享一份記憶體,一旦值被修改,則其他物件均對修改可見,故執行緒非安全。
(二)例項變數:單例時執行緒非安全,非單例時執行緒安全
1、例項變數:例項變數屬於類物件的,也就是說,屬於物件例項私有,在虛擬機器的堆中分配。
2、驗證例項變數的執行緒安全性:
(1)從程式截圖中,我們可以看到,當為單例模式時,會產生資源衝突,當非單例模式時,則不會產生執行緒衝突。
(2)程式執行結果圖:
圖1:
圖2:
3、結論:例項變數是例項物件私有的,系統只存在一個例項物件,則在多執行緒環境下,如果值改變後,則其它物件均可見,故執行緒非安全;如果每個執行緒都在不同的例項物件中執行,
則物件與物件間的修改互不影響,故執行緒安全。
(三)區域性變數:執行緒安全
1、區域性變數:定義在方法內部的變數。
2、驗證區域性變數的安全性:
(1)從程式截圖中可以看出,區域性變數在多執行緒下沒有產生資源衝突的問題
(2)程式執行結果圖:
3、結論:每個執行緒執行時都會把區域性變數放在各自的幀棧的記憶體空間中,執行緒間不共享,故不存線上程安全問題。
(四)靜態方法的執行緒安全性
1、靜態方法中如果沒有使用靜態變數,則沒有執行緒安全的問題;
靜態方法內的變數,每個執行緒呼叫時,都會新建立一份,不會公用一個儲存單元,故不存線上程衝突的問題。
以上就是對多執行緒環境下靜態變數、例項變數和區域性變數的一點點研究,也僅供自己在需要或遺忘的時候查詢參考下了。
SimpleDateFormat時間格式化存線上程安全問題
想必大家對SimpleDateFormat並不陌生。SimpleDateFormat 是 Java 中一個非常常用的類,該類用來對日期字串進行解析和格式化輸出,但如果使用不小心會導致非常微妙和難以除錯的問題,因為 DateFormat 和 SimpleDateFormat 類不都是執行緒安全的,在多執行緒環境下呼叫 format() 和 parse() 方法應該使用同步程式碼來避免問題。下面我們通過一個具體的場景來一步步的深入學習和理解SimpleDateFormat類。
一.引子
我們都是優秀的程式設計師,我們都知道在程式中我們應當儘量少的建立SimpleDateFormat 例項,因為建立這麼一個例項需要耗費很大的代價。在一個讀取資料庫資料匯出到excel檔案的例子當中,每次處理一個時間資訊的時候,就需要建立一個SimpleDateFormat例項物件,然後再丟棄這個物件。大量的物件就這樣被創建出來,佔用大量的記憶體和 jvm空間。程式碼如下:
package com.peidasoft.dateformat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateUtil { public static String formatDate(Date date)throws ParseException{ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } public static Date parse(String strDate) throws ParseException{ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(strDate); } }
你也許會說,OK,那我就建立一個靜態的simpleDateFormat例項,然後放到一個DateUtil類(如下)中,在使用時直接使用這個例項進行操作,這樣問題就解決了。改進後的程式碼如下:
package com.peidasoft.dateformat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateUtil { private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String formatDate(Date date)throws ParseException{ return sdf.format(date); } public static Date parse(String strDate) throws ParseException{ return sdf.parse(strDate); } }
當然,這個方法的確很不錯,在大部分的時間裡面都會工作得很好。但當你在生產環境中使用一段時間之後,你就會發現這麼一個事實:它不是執行緒安全的。在正常的測試情況之下,都沒有問題,但一旦在生產環境中一定負載情況下時,這個問題就出來了。他會出現各種不同的情況,比如轉化的時間不正確,比如報錯,比如執行緒被掛死等等。我們看下面的測試用例,那事實說話:
package com.peidasoft.dateformat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateUtil { private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String formatDate(Date date)throws ParseException{ return sdf.format(date); } public static Date parse(String strDate) throws ParseException{ return sdf.parse(strDate); } }
package com.peidasoft.dateformat; import java.text.ParseException; import java.util.Date; public class DateUtilTest { public static class TestSimpleDateFormatThreadSafe extends Thread { @Override public void run() { while(true) { try { this.join(2000); } catch (InterruptedException e1) { e1.printStackTrace(); } try { System.out.println(this.getName()+":"+DateUtil.parse("2013-05-24 06:02:20")); } catch (ParseException e) { e.printStackTrace(); } } } } public static void main(String[] args) { for(int i = 0; i < 3; i++){ new TestSimpleDateFormatThreadSafe().start(); } } }
執行輸出如下:
Exception in thread "Thread-1" java.lang.NumberFormatException: multiple points at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082) at java.lang.Double.parseDouble(Double.java:510) at java.text.DigitList.getDouble(DigitList.java:151) at java.text.DecimalFormat.parse(DecimalFormat.java:1302) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311) at java.text.DateFormat.parse(DateFormat.java:335) at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17) at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20) Exception in thread "Thread-0" java.lang.NumberFormatException: multiple points at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1082) at java.lang.Double.parseDouble(Double.java:510) at java.text.DigitList.getDouble(DigitList.java:151) at java.text.DecimalFormat.parse(DecimalFormat.java:1302) at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1589) at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1311) at java.text.DateFormat.parse(DateFormat.java:335) at com.peidasoft.orm.dateformat.DateNoStaticUtil.parse(DateNoStaticUtil.java:17) at com.peidasoft.orm.dateformat.DateUtilTest$TestSimpleDateFormatThreadSafe.run(DateUtilTest.java:20) Thread-2:Mon May 24 06:02:20 CST 2021 Thread-2:Fri May 24 06:02:20 CST 2013 Thread-2:Fri May 24 06:02:20 CST 2013 Thread-2:Fri May 24 06:02:20 CST 2013
說明:Thread-1和Thread-0報java.lang.NumberFormatException: multiple points錯誤,直接掛死,沒起來;Thread-2 雖然沒有掛死,但輸出的時間是有錯誤的,比如我們輸入的時間是:2013-05-24 06:02:20 ,當會輸出:Mon May 24 06:02:20 CST 2021 這樣的靈異事件。
二.原因
作為一個專業程式設計師,我們當然都知道,相比於共享一個變數的開銷要比每次建立一個新變數要小很多。上面的優化過的靜態的SimpleDateFormat版,之所在併發情況下回出現各種靈異錯誤,是因為SimpleDateFormat和DateFormat類不是執行緒安全的。我們之所以忽視執行緒安全的問題,是因為從SimpleDateFormat和DateFormat類提供給我們的介面上來看,實在讓人看不出它與執行緒安全有何相干。只是在JDK文件的最下面有如下說明:
SimpleDateFormat中的日期格式不是同步的。推薦(建議)為每個執行緒建立獨立的格式例項。如果多個執行緒同時訪問一個格式,則它必須保持外部同步。
JDK原始文件如下:
Synchronization:
Date formats are not synchronized.
It is recommended to create separate format instances for each thread.
If multiple threads access a format concurrently, it must be synchronized externally.
下面我們通過看JDK原始碼來看看為什麼SimpleDateFormat和DateFormat類不是執行緒安全的真正原因:
SimpleDateFormat繼承了DateFormat,在DateFormat中定義了一個protected屬性的 Calendar類的物件:calendar。只是因為Calendar累的概念複雜,牽扯到時區與本地化等等,Jdk的實現中使用了成員變數來傳遞引數,這就造成在多執行緒的時候會出現錯誤。
在format方法裡,有這樣一段程式碼:
private StringBuffer format(Date date, StringBuffer toAppendTo, FieldDelegate delegate) { // Convert input date to time field list calendar.setTime(date); boolean useDateFormatSymbols = useDateFormatSymbols(); for (int i = 0; i < compiledPattern.length; ) { int tag = compiledPattern[i] >>> 8; int count = compiledPattern[i++] & 0xff; if (count == 255) { count = compiledPattern[i++] << 16; count |= compiledPattern[i++]; } switch (tag) { case TAG_QUOTE_ASCII_CHAR: toAppendTo.append((char)count); break; case TAG_QUOTE_CHARS: toAppendTo.append(compiledPattern, i, count); i += count; break; default: subFormat(tag, count, delegate, toAppendTo, useDateFormatSymbols); break; } } return toAppendTo; }
calendar.setTime(date)這條語句改變了calendar,稍後,calendar還會用到(在subFormat方法裡),而這就是引發問題的根源。想象一下,在一個多執行緒環境下,有兩個執行緒持有了同一個SimpleDateFormat的例項,分別呼叫format方法:
執行緒1呼叫format方法,改變了calendar這個欄位。
中斷來了。
執行緒2開始執行,它也改變了calendar。
又中斷了。
執行緒1回來了,此時,calendar已然不是它所設的值,而是走上了執行緒2設計的道路。如果多個執行緒同時爭搶calendar物件,則會出現各種問題,時間不對,執行緒掛死等等。
分析一下format的實現,我們不難發現,用到成員變數calendar,唯一的好處,就是在呼叫subFormat時,少了一個引數,卻帶來了這許多的問題。其實,只要在這裡用一個區域性變數,一路傳遞下去,所有問題都將迎刃而解。
這個問題背後隱藏著一個更為重要的問題--無狀態:無狀態方法的好處之一,就是它在各種環境下,都可以安全的呼叫。衡量一個方法是否是有狀態的,就看它是否改動了其它的東西,比如全域性變數,比如例項的欄位。format方法在執行過程中改動了SimpleDateFormat的calendar欄位,所以,它是有狀態的。
這也同時提醒我們在開發和設計系統的時候注意下一下三點:
1.自己寫公用類的時候,要對多執行緒呼叫情況下的後果在註釋裡進行明確說明
2.對執行緒環境下,對每一個共享的可變變數都要注意其執行緒安全性
3.我們的類和方法在做設計的時候,要儘量設計成無狀態的
三.解決辦法
1.需要的時候建立新例項:
package com.peidasoft.dateformat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateUtil { public static String formatDate(Date date)throws ParseException{ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } public static Date parse(String strDate) throws ParseException{ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.parse(strDate); } }
說明:在需要用到SimpleDateFormat 的地方新建一個例項,不管什麼時候,將有執行緒安全問題的物件由共享變為區域性私有都能避免多執行緒問題,不過也加重了建立物件的負擔。在一般情況下,這樣其實對效能影響比不是很明顯的。
2.使用同步:同步SimpleDateFormat物件
package com.peidasoft.dateformat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class DateSyncUtil { private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public static String formatDate(Date date)throws ParseException{ synchronized(sdf){ return sdf.format(date); } } public static Date parse(String strDate) throws ParseException{ synchronized(sdf){ return sdf.parse(strDate); } } }
說明:當執行緒較多時,當一個執行緒呼叫該方法時,其他想要呼叫此方法的執行緒就要block,多執行緒併發量大的時候會對效能有一定的影響。
3.使用ThreadLocal:
package com.peidasoft.dateformat; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class ConcurrentDateUtil { private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; public static Date parse(String dateStr) throws ParseException { return threadLocal.get().parse(dateStr); } public static String format(Date date) { return threadLocal.get().format(date); } }
另外一種寫法:
package com.peidasoft.dateformat; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; public class ThreadLocalDateUtil { private static final String date_format = "yyyy-MM-dd HH:mm:ss"; private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(); public static DateFormat getDateFormat() { DateFormat df = threadLocal.get(); if(df==null){ df = new SimpleDateFormat(date_format); threadLocal.set(df); } return df; } public static String formatDate(Date date) throws ParseException { return getDateFormat().format(date); } public static Date parse(String strDate) throws ParseException { return getDateFormat().parse(strDate); } }
說明:使用ThreadLocal, 也是將共享變數變為獨享,執行緒獨享肯定能比方法獨享在併發環境中能減少不少建立物件的開銷。如果對效能要求比較高的情況下,一般推薦使用這種方法。
4.拋棄JDK,使用其他類庫中的時間格式化類:
1.使用Apache commons 裡的FastDateFormat,宣稱是既快又執行緒安全的SimpleDateFormat, 可惜它只能對日期進行format, 不能對日期串進行解析。
2.使用Joda-Time類庫來處理時間相關問題
做一個簡單的壓力測試,方法一最慢,方法三最快,但是就算是最慢的方法一效能也不差,一般系統方法一和方法二就可以滿足,所以說在這個點很難成為你係統的瓶頸所在。從簡單的角度來說,建議使用方法一或者方法二,如果在必要的時候,追求那麼一點效能提升的話,可以考慮用方法三,用ThreadLocal做快取。
Joda-Time類庫對時間處理方式比較完美,建議使用。
參考資料:
1.http://dreamhead.blogbus.com/logs/215637834.html
2.http://www.blogjava.net/killme2008/archive/2011/07/10/354062.html
出處:http://www.cnblogs.com/peida/archive/2013/05/31/3070790.html
標籤: SimpleDateFormat