Spring單例模式與線程安全

  • 問題背景

這段時間在作項目的時候,考慮到Spring中的bean默認是單例模式的,那麼當多個線程調用同一個bean的時候就會存在線程安全問題。若是是Spring中bean的建立模式爲非單例的,也就不存在這樣的問題了。java

  • Spring 單例模式與線程安全

Spring 框架裏的 bean ,或者說組件,獲取實例的時候都是默認的單例模式,這是在多線程開發的時候要尤爲注意的地方。 單例模式的意思就是隻有一個實例。單例模式確保某一個類只有一個實例,並且自行實例化並向整個系統提供這個實例。這個類稱爲單例類。緩存

當多用戶同時請求一個服務時,容器會給每個請求分配一個線程,這是多個線程會併發執行該請求多對應的業務邏輯(成員方法),此時就要注意了,若是該處理邏輯中有對該單列狀態的修改(體現爲該單列的成員屬性),則必須考慮線程同步問題。安全

同步機制的比較: ThreadLocal 和線程同步機制相比有什麼優點呢? ThreadLocal和線程同步機制都是爲了解決多線程中相同變量的訪問衝突問題。  多線程

在同步機制中,經過對象的鎖機制保證同一時間只有一個線程訪問變量。這時該變量是多個線程共享的,使用同步機制要求程序慎密地分析何時對變量進行讀寫,何時須要鎖定某個對象,何時釋放對象鎖等繁雜的問題,程序設計和編寫難度相對較大。併發

而 ThreadLocal 則從另外一個角度來解決多線程的併發訪問。 ThreadLocal 會爲每個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問衝突。由於每個線程都擁有本身的變量副本,從而也就沒有必要對該變量進行同步了。 ThreadLocal提供了線程安全的共享對象,在編寫多線程代碼時,能夠把不安全的變量封裝進ThreadLocal 。  框架

因爲 ThreadLocal 中能夠持有任何類型的對象,低版本 JDK 所提供的 get() 返回的是 Object 對象,須要強制類型轉換。但 JDK 5.0 經過泛型很好的解決了這個問題,在必定程度地簡化 ThreadLocal 的使用。 括起來講,對於多線程資源共享的問題,同步機制採用了 「 以時間換空間 」 的方式,而 ThreadLocal 採用了 「 以空間換時間 」 的方式。前者僅提供一份變量,讓不一樣的線程排隊訪問,然後者爲每個線程都提供了一份變量,所以能夠同時訪問而互不影響。  less

Spring 使用 ThreadLocal 解決線程安全問題  。 咱們知道在通常狀況下,只有無狀態的 Bean 才能夠在多線程環境下共享,在 Spring 中,絕大部分 Bean 均可以聲明爲singleton 做用域。就是由於 Spring 對一些 Bean (如 RequestContextHolder 、TransactionSynchronizationManager 、 LocaleContextHolder 等)中非線程安全狀態採用 ThreadLocal 進行處理,讓它們也成爲線程安全的狀態,由於有狀態的 Bean 就能夠在多線程中共享了。  ide

通常的 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 類庫對時間處理方式比較完美,建議使用。

相關文章
相關標籤/搜索