Spring bean 和單例bean的執行緒安全
-
Bean的作用域
Spring 3中為Bean定義了5中作用域,分別為singleton(單例)、prototype(原型)、request、session和global session,5種作用域說明如下:
- singleton:單例模式,Spring IoC容器中只會存在一個共享的Bean例項,無論有多少個Bean引用它,始終指向同一物件。Singleton作用域是Spring中的預設作用域,也可以顯示的將Bean定義為singleton模式,配置為:
- <bean id="userDao" class="com.ioc.UserDaoImpl" scope="singleton"/>
- prototype:原型模式,每次通過Spring容器獲取prototype定義的bean時,容器都將建立一個新的Bean例項,每個Bean例項都有自己的屬性和狀態,而singleton全域性只有一個物件。根據經驗,對有狀態的bean使用prototype作用域,而對無狀態的bean使用singleton作用域。
- request:在一次Http請求中,容器會返回該Bean的同一例項。而對不同的Http請求則會產生新的Bean,而且該bean僅在當前Http Request內有效。
- <bean id="loginAction" class="com.cnblogs.Login" scope="request"/>,針對每一次Http請求,Spring容器根據該bean的定義建立一個全新的例項,且該例項僅在當前Http請求內有效,而其它請求無法看到當前請求中狀態的變化,噹噹前Http請求結束,該bean例項也將會被銷燬。
- session:在一次Http Session中,容器會返回該Bean的同一例項。而對不同的Session請求則會建立新的例項,該bean例項僅在當前Session內有效。
- <bean id="userPreference" class="com.ioc.UserPreference" scope="session"/>,同Http請求相同,每一次session請求建立新的例項,而不同的例項之間不共享屬性,且例項僅在自己的session請求內有效,請求結束,則例項將被銷燬。
- global Session:在一個全域性的Http Session中,容器會返回該Bean的同一個例項,僅在使用portlet context時有效。
- Bean的生命週期
經過如上對Bean作用域的介紹,接下來將在Bean作用域的基礎上講解Bean的生命週期。
Spring容器可以管理singleton作用域下Bean的生命週期,在此作用域下,Spring能夠精確地知道Bean何時被建立,何時初始化完成,以及何時被銷燬。而對於prototype作用域的Bean,Spring只負責建立,當容器建立了Bean的例項後,Bean的例項就交給了客戶端的程式碼管理,Spring容器將不再跟蹤其生命週期,並且不會管理那些被配置成prototype作用域的Bean的生命週期。Spring中Bean的生命週期的執行是一個很複雜的過程,讀者可以利用Spring提供的方法來定製Bean的建立過程。Spring容器在保證一個bean例項能夠使用之前會做很多工作:
singleton是單態模式的 ,有ioc容器管理 ,當然不是執行緒安全的啦 ,不過所謂的執行緒安全也是相對的,如果你的類是沒有狀態的(沒有類的公用屬性,不會同時被多個執行緒改變), 那用singleton 的效能要高一些 ,因為只有一個例項 。
如果你的類是有狀態的(有公用屬性,可能被不同的執行緒改變),那就必須顯示的設定為prototype了。
Spring單例模式及執行緒安全
Spring框架中的Bean,或者說元件,獲取例項的時候都是預設單例模式,這是在多執行緒開發的時候需要尤其注意的地方。
單例模式的意思是隻有一個例項,例如在Spring容器中某一個類只有一個例項,而且自行例項化後並項整個系統提供這個例項,這個類稱為單例類。
當多個使用者同時請求一個服務時,容器會給每一個請求分配一個執行緒,這時多個執行緒會併發執行該請求對應的業務邏輯(成員方法),此時就要注意了,如果該處理邏輯中有對單例狀態的修改(體現為該單例的成員屬性),則必須考慮執行緒同步問題。
同步機制的比較:
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引用,非執行緒安全的。
這個問題背後隱藏著一個更為重要的問題--無狀態:無狀態方法的好處之一,就是它在各種環境下,都可以安全的呼叫。衡量一個方法是否是有狀態的,就看它是否改動了其它的東西,比如全域性變數,比如例項的欄位。format方法在執行過程中改動了SimpleDateFormat的calendar欄位,所以,它是有狀態的。
這也同時提醒我們在開發和設計系統的時候注意下一下三點:
1.自己寫公用類的時候,要對多執行緒呼叫情況下的後果在註釋裡進行明確說明
2.對執行緒環境下,對每一個共享的可變變數都要注意其執行緒安全性
3.我們的類和方法在做設計的時候,要儘量設計成無狀態的
解決方法:
1.使用synchronized關鍵字進行資料同步,或者使用ThreadLocal保證執行緒安全
2.不適用JDK自帶的時間格式化類,使用其他類庫的
- 使用Apache commons裡的FastDateFormat,宣城是既快有執行緒安全的SimpleDateFormat,可惜他只能對日期進行format,不能對日期串進行解析
- 使用Joda-Time類庫來處理時間相關問題,該種對時間的處理方式比較完美,建議使用。