- 問題背景
這段時間在做項目的時候,考慮到Spring中的bean默認是單例模式的,那么當多個線程調用同一個bean的時候就會存在線程安全問題。如果是Spring中bean的創建模式為非單例的,也就不存在這樣的問題了。
- Spring 單例模式與線程安全
Spring 框架里的 bean ,或者說組件,獲取實例的時候都是默認的單例模式,這是在多線程開發的時候要尤其注意的地方。 單例模式的意思就是只有一個實例。單例模式確保某一個類只有一個實例,而且自行實例化並向整個系統提供這個實例。這個類稱為單例類。
當多用戶同時請求一個服務時,容器會給每一個請求分配一個線程,這是多個線程會並發執行該請求多對應的業務邏輯(成員方法),此時就要注意了,如果該處理邏輯中有對該單列狀態的修改(體現為該單列的成員屬性),則必須考慮線程同步問題。
同步機制的比較: ThreadLocal 和線程同步機制相比有什么優勢呢? ThreadLocal和線程同步機制都是為了解決多線程中相同變量的訪問沖突問題。
在同步機制中,通過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析什么時候對變量進行讀寫,什么時候需要鎖定某個對象,什么時候釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。
而 ThreadLocal 則從另一個角度來解決多線程的並發訪問。 ThreadLocal 會為每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問沖突。因為每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。 ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,可以把不安全的變量封裝進ThreadLocal 。
由於 ThreadLocal 中可以持有任何類型的對象,低版本 JDK 所提供的 get() 返回的是 Object 對象,需要強制類型轉換。但 JDK 5.0 通過泛型很好的解決了這個問題,在一定程度地簡化 ThreadLocal 的使用。 括起來說,對於多線程資源共享的問題,同步機制采用了 “ 以時間換空間 ” 的方式,而 ThreadLocal 采用了 “ 以空間換時間 ” 的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
Spring 使用 ThreadLocal 解決線程安全問題 。 我們知道在一般情況下,只有無狀態的 Bean 才可以在多線程環境下共享,在 Spring 中,絕大部分 Bean 都可以聲明為singleton 作用域。就是因為 Spring 對一些 Bean (如 RequestContextHolder 、TransactionSynchronizationManager 、 LocaleContextHolder 等)中非線程安全狀態采用 ThreadLocal 進行處理,讓它們也成為線程安全的狀態,因為有狀態的 Bean 就可以在多線程中共享了。
一般的 Web 應用划分為展現層、服務層和持久層三個層次,在不同的層中編寫對應的邏輯,下層通過接口向上層開放功能調用。在一般情況下,從接收請求到返回響應所經過的所有程序調用都同屬於一個線程。
ThreadLocal 是解決線程安全問題一個很好的思路,它通過為每個線程提供一個獨立的變量副本解決了變量並發訪問的沖突問題。在很多情況下, ThreadLocal 比直接使用 synchronized 同步機制解決線程安全問題更簡單,更方便,且結果程序擁有更高的並發性。
如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。 或者說 : 一個類或者程序所提供的接口對於線程來說是原子操作或者多個線程之間的切換不會導致該接口的執行結果存在二義性 , 也就是說我們不用考慮同步的問題。 線程安全問題都是由全局變量及靜態變量引起的。
若每個線程中對全局變量、靜態變量只有讀操作,而無寫操作,一般來說,這個全局變量是線程安全的;若有多個線程同時執行寫操作,一般都需要考慮線程同步,否則就可能影響線程安全。
1 ) 常量始終是線程安全的,因為只存在讀操作。
2 )每次調用方法前都新建一個實例是線程安全的,因為不會訪問共享的資源。
3 )局部變量是線程安全的。因為每執行一個方法,都會在獨立的空間創建局部變量,它不是共享的資源。局部變量包括方法的參數變量和方法內變量。
有狀態就是有數據存儲功能。有狀態對象 (Stateful Bean) ,就是有實例變量的對象 ,可以保存數據,是非線程安全的。在不同方法調用間不保留任何狀態。
無狀態就是一次操作,不能保存數據。無狀態對象 (Stateless Bean) ,就是沒有實例變量的對象 . 不能保存數據,是不變類,是線程安全的。
有狀態對象 :
無狀態的 Bean 適合用不變模式,技術就是單例模式,這樣可以共享實例,提高性能。有狀態的 Bean ,多線程環境下不安全,那么適合用 Prototype 原型模式。Prototype: 每次對 bean 的請求都會創建一個新的 bean 實例。
Struts2 默認的實現是 Prototype 模式。也就是每個請求都新生成一個 Action 實例,所以不存在線程安全問題。需要注意的是,如果由 Spring 管理 action 的生命周期, scope 要配成 prototype 作用域。
- 線程安全案例
SimpleDateFormat( 下面簡稱 sdf) 類內部有一個 Calendar 對象引用 , 它用來儲存和這個 sdf 相關的日期信息 , 例如 sdf.parse(dateStr), sdf.format(date) 諸如此類的方法參數傳入的日期相關 String, Date 等等 , 都是交友 Calendar 引用來儲存的 . 這樣就會導致一個問題 , 如果你的 sdf 是個 static 的 , 那么多個 thread 之間就會共享這個 sdf, 同時也是共享這個 Calendar 引用 , 並且 , 觀察 sdf.parse() 方法 , 你會發現有如下的調用 :
1 Date parse() { 2 calendar.clear(); // 清理calendar 3 ... // 執行一些操作, 設置 calendar 的日期什么的 4 calendar.getTime(); // 獲取calendar的時間 5 }
這里會導致的問題就是 , 如果 線程 A 調用了 sdf.parse(), 並且進行了 calendar.clear() 后還未執行 calendar.getTime() 的時候 , 線程 B 又調用了 sdf.parse(), 這時候線程 B 也執行了 sdf.clear() 方法 , 這樣就導致線程 A 的的 calendar 數據被清空了 ( 實際上 A,B 的同時被清空了 ). 又或者當 A 執行了 calendar.clear() 后被掛起 , 這時候 B 開始調用 sdf.parse() 並順利 i 結束 , 這樣 A 的 calendar 內存儲的的 date 變成了后來 B 設置的 calendar 的 date
這個問題背后隱藏着一個更為重要的問題 -- 無狀態:無狀態方法的好處之一,就是它在各種環境下,都可以安全的調用。衡量一個方法是否是有狀態的,就看它是否改動了其它的東西,比如全局變量,比如實例的字段。 format 方法在運行過程中改動了SimpleDateFormat 的 calendar 字段,所以,它是有狀態的。
這也同時提醒我們在開發和設計系統的時候注意下一下三點 :
1. 自己寫公用類的時候,要對多線程調用情況下的后果在注釋里進行明確說明
2. 對線程環境下,對每一個共享的可變變量都要注意其線程安全性
3. 我們的類和方法在做設計的時候,要盡量設計成無狀態的
- 解決辦法
1. 需要的時候創建新實例:
說明:在需要用到 SimpleDateFormat 的地方新建一個實例,不管什么時候,將有線程安全問題的對象由共享變為局部私有都能避免多線程問題,不過也加重了創建對象的負擔。在一般情況下,這樣其實對性能影響比不是很明顯的。
2. 使用同步:同步 SimpleDateFormat 對象
1 public class DateSyncUtil { 2 private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 3 4 public static String formatDate(Date date)throws ParseException{ 5 synchronized(sdf){ 6 return sdf.format(date); 7 } 8 } 9 10 public static Date parse(String strDate) throws ParseException{ 11 synchronized(sdf){ 12 return sdf.parse(strDate); 13 } 14 } 15 }
說明:當線程較多時,當一個線程調用該方法時,其他想要調用此方法的線程就要block ,多線程並發量大的時候會對性能有一定的影響。
3. 使用 ThreadLocal :
1 public class ConcurrentDateUtil { 2 private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() { 3 @Override 4 protected DateFormat initialValue() { 5 return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 6 } 7 }; 8 public static Date parse(String dateStr) throws ParseException { 9 return threadLocal.get().parse(dateStr); 10 } 11 public static String format(Date date) { 12 return threadLocal.get().format(date); 13 } 14 }
或
1 ThreadLocal<DateFormat>(); 2 3 public static DateFormat getDateFormat() 4 { 5 DateFormat df = threadLocal.get(); 6 if(df==null){ 7 df = new SimpleDateFormat(date_format); 8 threadLocal.set(df); 9 } 10 return df; 11 } 12 public static String formatDate(Date date) throws ParseException { 13 return getDateFormat().format(date); 14 } 15 public static Date parse(String strDate) throws ParseException { 16 return getDateFormat().parse(strDate); 17 } 18 }
說明:使用 ThreadLocal, 也是將共享變量變為獨享,線程獨享肯定能比方法獨享在並發環境中能減少不少創建對象的開銷。如果對性能要求比較高的情況下,一般推薦使用這種方法。
4. 拋棄 JDK ,使用其他類庫中的時間格式化類:
1. 使用 Apache commons 里的 FastDateFormat ,宣稱是既快又線程安全的SimpleDateFormat, 可惜它只能對日期進行 format, 不能對日期串進行解析。
2. 使用 Joda-Time 類庫來處理時間相關問題
做一個簡單的壓力測試,方法一最慢,方法三最快,但是就算是最慢的方法一性能也不差,一般系統方法一和方法二就可以滿足,所以說在這個點很難成為你系統的瓶頸所在。從簡單的角度來說,建議使用方法一或者方法二,如果在必要的時候,追求那么一點性能提升的話,可以考慮用方法三,用 ThreadLocal 做緩存。
Joda-Time 類庫對時間處理方式比較完美,建議使用。