Spring單例Bean和線程安全


Spring的bean默認都是單例的,這些單例Bean在多線程程序下如何保證線程安全呢?例如對於Web應用來說,Web容器對於每個用戶請求都創建一個單獨的Sevlet線程來處理請求,引入Spring框架之后,每個Action都是單例的,那么對於Spring托管的單例Service Bean,如何保證其安全呢?本文介紹了以上的安全問題。

Spring的原型Bean與單例Bean的設置

spring單例Bean

spring中的Bean缺省的情況下是單例模式的,在spring容器中分配Bean的時候(無論通過getBean()還是通過依賴注入(IOC)),它總是返回同一個Bean的實例,如果你想每次向上下文請求一個bean的時候總是得到一個不同的實例,或者想每次想從spring容器中得到一個bean的不同實例,需要將bean定義為原型模式,定義為原型模式意味着你是定義一個bean的類,而不是一個單一的bean的實例,bean的實例都是按照這個類而創建的。

spring原型Bean
在spring中<bean>的singleton屬性告訴上下文這個bean是原型bean或者是單例bean。bean的缺省值為 true,如果設為false的話,就把這個bean定義成了原型bean。例如:<beanid=”test” class=”demo.Demo” singleton=”false” />

在spring2.x中<bean id=”test”scope=”prototype”/>將這樣配置,但是如果想使用spring的原型bean必須通過getBean(”test”)這樣的方 式,而不能通過使用IOC方式,因為:getBean將每次都有spring來裝配轉發,而IOC將只是一次注入的目標bean中,以后不再重新注入。這 樣通過getBean方式將得到一個原型bean。如果bean使用的是有限資源,如數據庫和網絡鏈接的話不需要使用原型bean,正常不要把 singleton=”false”或者scope=”prototype”除非必要。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作用域。

 

Spring 單例Bean和Java 單例模式的區別

Spring的的單例是基於BeanFactory也就是spring容器,單例Bean在此Spring容器內是單個的,Java的單例是基於JVM,每個JVM內一個單例。

 

線程安全

Thread safety is a computer programming concept applicable in thecontext of multi-threaded programs. A piece of codeis thread-safe if it can be safely invoked by multiple threads at thesame time [1].

Thread safety is a key challenge in multi-threadedprogramming. It was not a concern for most application programmers of littlehome applications, but since the 1990s, as Windows became multithreaded, andwith the expansion of BSD and Linux operating systems, it has become acommonplace issue. In a multi-threaded program, several threads executesimultaneously in a shared address space. Every thread has access to virtuallyall the memory of every other thread. Thus the flow ofcontrol and the sequence of accesses to data often have little relation to whatwould be reasonably expected by looking at the text of the program, violatingthe principle of least astonishment.Thread safety is a property that allows code to run in multi-threadedenvironments by re-establishing some of the correspondences between the actualflow of control and the text of the program, by means ofProcess synchronization.

Identification

It is not easy to determine if a piece of code isthread-safe or not. However, there are several indicators that suggest the needfor careful examination to see if it is unsafe:

 Implementation

There are a few ways to achieve thread safety:

Re-entrancy 

Writing code insuch a way that it can be partially executed by one task, reentered by another task, and thenresumed from the original task. This requires the saving of state information in variables local toeach task, usually on its stack, instead of in staticor global variables. There are still some rare caseswhere a static variable can be used in a reentrant function, if the access isdone through atomic operations.

Mutualexclusion or Process synchronization

Access to shareddata is serialized using mechanisms that ensure only one thread reads orwrites the shared data at any time. Great care is required if a piece of codeaccesses multiple shared pieces of data—problems include raceconditionsdeadlocks,livelocksand starvation.

Thread-local storage 

Variables arelocalized so that each thread has its own private copy. These variables retaintheir values across subroutine and other code boundaries, and are thread-safesince they are local to each thread, even though the code which accesses themmight be reentrant.

Atomicoperations 

Shared data areaccessed by using atomic operations which cannot be interrupted by otherthreads. This usually requires using specialmachinelanguage instructions, which might be available in a runtimelibrary. Since the operations are atomic, the shared data are always keptin a valid state, no matter what other threads access it. Atomicoperations form the basis of many thread locking mechanisms.

 Examples

In the following piece of C code, the function is thread-safe, butnot reentrant

int function()
{
        mutex_lock();
        ...
        function body
        ...
        mutex_unlock();
}

In the above, function can be called bydifferent threads without any problem. But if the function is used in areentrant interrupt handler and a second interrupt arises inside the function,the second routine will hang forever. As interrupt servicing can disable otherinterrupts, the whole system could suffer.

Concurrent programing

Note that a piece of code can be thread safe, and yet notbeing able to run at the same time that some other piece of code is running. Atrivial example of that is when that other piece of code restarts the computer.The following piece of C code, presents a less obvious situationwhere a thread is using a file that another thread or process might delete.

int function()
{
char *filename = "/etc/config";
FILE *config;
        if (file_exist(filename)){
                config = fopen(filename);
        }
}

In the above, the function is thread-safe, as it can becalled from any number of threads and will not fail. But all the calls shouldbe in a controlled environment. If executed in a multi-process environment, orif the file is stored on a network-shared drive, there is no warranty that itwon't be deleted.

Difficulties

One approach to making data thread-safe that combinesseveral of the above elements is to make changes atomicallyto update the shared data. Thus, most of the code is concurrent, and little time is spentserialized.

 

 可重入函數與不可重入函數

主要用於多任務環境中,一個可重入的函數簡單來說就是可以被中斷的函數,也就是說,可以在這個函數執行的任何時刻中斷它,轉入OS調度下去執行另外一段代碼,而返回控制時不會出現什么錯誤;而不可重入的函數由於使用了一些系統資源,比如全局變量區,中斷向量表等,所以它如果被中斷的話,可能會出現問題,這類函數是不能運行在多任務環境下的。

也可以這樣理解,重入即表示重復進入,首先它意味着這個函數可以被中斷,其次意味着它除了使用自己棧上的變量以外不依賴於任何環境(包括static),這樣的函數就是purecode(純代碼)可重入,可以允許有該函數的多個副本在運行,由於它們使用的是分離的棧,所以不會互相干擾。如果確實需要訪問全局變量(包括static),一定要注意實施互斥手段。可重入函數在並行運行環境中非常重要,但是一般要為訪問全局變量付出一些性能代價。

編寫可重入函數時,若使用全局變量,則應通過關中斷、信號量(即P、V操作)等手段對其加以保護。

 說明:若對所使用的全局變量不加以保護,則此函數就不具有可重入性,即當多個進程調用此函數時,很有可能使有關全局變量變為不可知狀態。

 

示例:假設Exam是int型全局變量,函數Squre_Exam返回Exam平方值。那么如下函數不具有可重入性。

unsigned int example( int para )

{

    unsigned int temp;
        Exam = para; // (**)
        temp = Square_Exam( );
        return temp;
    }
    此函數若被多個進程調用的話,其結果可能是未知的,因為當(**)語句剛執行完后,另外一個使用本函數的進程可能正好被激活,那么當新激活的進程執行到此函數時,將使Exam賦與另一個不同的para值,所以當控制重新回到“temp = Square_Exam( )”后,計算出的temp很可能不是預想中的結果。此函數應如下改進。

    unsigned int example( int para ) {
        unsigned int temp;
        [申請信號量操作] //(1)
        Exam = para;
        temp = Square_Exam( );
        [釋放信號量操作]
        return temp;
    }
    (1)若申請不到“信號量”,說明另外的進程正處於給Exam賦值並計算其平方過程中(即正在使用此信號),本進程必須等待其釋放信號后,才可繼續執行。若申請到信號,則可繼續執行,但其它進程必須等待本進程釋放信號量后,才能再使用本信號。

    保證函數的可重入性的方法:
    在寫函數時候盡量使用局部變量(例如寄存器、堆棧中的變量),對於要使用的全局變量要加以保護(如采取關中斷、信號量等方法),這樣構成的函數就一定是一個可重入的函數。
    VxWorks中采取的可重入的技術有:
    * 動態堆棧變量(各子函數有自己獨立的堆棧空間)
    * 受保護的全局變量和靜態變量
    * 任務變量


--------------------------------------------------
    在 實時系統的設計中,經常會出現多個任務調用同一個函數的情況。如果這個函數不幸被設計成為不可重入的函數的話,那么不同任務調用這個函數時可能修改其他任務調用這個函數的數據,從而導致不可預料的后果。那么什么是可重入函數呢?所謂可重入函數是指一個可以被多個任務調用的過程,任務在調用時不必擔心數據是 否會出錯。不可重入函數在實時系統設計中被視為不安全函數。滿足下列條件的函數多數是可重入的
    1) 函數體內使用了靜態的數據結構;
    2) 函數體內調用了malloc()或者free()函數;
    3) 函數體內調用了標准I/O函數。

    下面舉例加以說明。
    A. 可重入函數
    void strcpy(char *lpszDest, char *lpszSrc)

 {
        while(*lpszDest++=*lpszSrc++);
        *dest=0;
    }

    B. 不可重入函數1
    charcTemp;//全局變量
    void SwapChar1(char *lpcX, char *lpcY)

 {
        cTemp=*lpcX;
        *lpcX=*lpcY;
        lpcY=cTemp;//訪問了全局變量
    }

    C. 不可重入函數2
    void SwapChar2(char *lpcX,char *lpcY)

 {
        static char cTemp;//靜態局部變量
        cTemp=*lpcX;
        *lpcX=*lpcY;
        lpcY=cTemp;//使用了靜態局部變量
    }

    問題1,如何編寫可重入的函數?
    答:在函數體內不訪問那些全局變量,不使用靜態局部變量,堅持只使用局部變量,寫出的函數就將是可重入的。如果必須訪問全局變量,記住利用互斥信號量來保護全局變量。

    問題2,如何將一個不可重入的函數改寫成可重入的函數?
    答:把一個不可重入函數變成可重入的唯一方法是用可重入規則來重寫它。其實很簡單,只要遵守了幾條很容易理解的規則,那么寫出來的函數就是可重入的。
    1) 不要使用全局變量。因為別的代碼很可能覆蓋這些變量值。
    2) 在和硬件發生交互的時候,切記執行類似disinterrupt()之類的操作,就是關閉硬件中斷。完成交互記得打開中斷,在有些系列上,這叫做“進入/退出核心”。
    3) 不能調用其它任何不可重入的函數。
    4) 謹慎使用堆棧。最好先在使用前先OS_ENTER_KERNAL。

    堆棧操作涉及內存分配,稍不留神就會造成益出導致覆蓋其他任務的數據,所以,請謹慎使用堆棧!最好別用!很多黑客程序就利用了這一點以便系統執行非法代碼從而輕松獲得系統控制權。還有一些規則,總之,時刻記住一句話:保證中斷是安全的!

    實例問題:曾經設計過如下一個函數,在代碼檢視的時候被提醒有bug,因為這個函數是不可重入的,為什么?
    unsigned int sum_int( unsigned int base )

{
        unsigned int index;
        static unsigned int sum = 0; // 注意,是static類型
        for (index = 1; index <= base;index++)
            sum += index;
        return sum;
    }

    分析:所謂的函數是可重入的(也可以說是可預測的),即只要輸入數據相同就應產生相同的輸出。這個函數之所以是不可預測的,就是因為函數中使用了static變量,因為static變量的特征,這樣的函數被稱為:帶“內部存儲器”功能的的函數。因此如果需要一個可重入的函數,一定要避免函數中使用static變量,這種函數中的static變量,使用原則是,能不用盡量不用。
    將上面的函數修改為可重入的函數,只要將聲明sum變量中的static關鍵字去掉,變量sum即變為一個auto類型的變量,函數即變為一個可重入的函數。
    當然,有些時候,在函數中是必須要使用static變量的,比如當某函數的返回值為指針類型時,則必須是static的局部變量的地址作為返回值,若為auto類型,則返回為錯指針。

 

Spring單例的線程安全

 基本上,Spring的thread-safe是其API自身的thread-safe。比如一個常見的場景(from appfuse):
    public class UserManagerImpl extends BaseManager implements UserManager {
        private UserDao dao;
    ......
    public class UserDaoHibernate extends BaseDaoHibernate implements UserDao, UserDetailsService {
    ......
    public class BaseDaoHibernate extends HibernateDaoSupport implements Dao {
    這些bean都是Singleton的。
    一個類如果沒有成員變量,那這個類肯定是thread-safe的,所以UserDaoHibernate的thread-safe取決於其父類。而 UserManagerImpl 的安全性又取決於UserDaoHibernate,最后是HibernateTemplate。可以看出,這里的bean都小心翼翼的維護其成員變量, 或者基本沒有成員變量,而將thread-safe轉嫁給Spring的API。如果開發者按照約定的或者用自動產生的工具(appgen不錯)來編寫數 據訪問層,是沒有線程安全性的問題的。Spring本身不提供這方面的保證。
    或者bean的定義為Singletons="false",也可以參考前面的一篇文章Thread safety, singletons and Spring,用lookup-method。<pro spring> charpter 5介紹的更詳細:
    Lookup Method Injection was added to Spring to overcome the problems encountered when a bean depends on another bean with a different lifecycle—specifically, when a singleton depends on a non-singleton. In this situation, both setter and constructor injection result in the singleton maintaining a single instance of what should be a non-singleton bean. In some cases, you will want to have the singleton bean obtain a new instance of the non-singleton every time it requires the bean in question.
    顯然,如果A(Singletons) depends B(Propotype),使用這種方式可以避免A對B的訪問並發和爭用的問題。
     <pro spring>這本書也對Singletons=“true/false"的選擇做了個小結:
    使用Singletons的情況有:
    1.Shared objects with no state;
    2.Shared object with read-only state;
    3.Shared object with shared state;
    4.High throughput objects with writable state. (synchronizing is need)
    使用propotype的情況有:
    1.Objects with writable stat;
    2.Objects with private state.

    與Spring的高度靈活不同,EJB的規范將同步作為一個服務(one of primary services),開發者開編寫bean時不必考慮(也不能)線程相關的問題。session bean其分為兩類,也有同步上的考慮。
    雖然thread-safe的問題總是存在,EJB也沒有從本質上解決這個問題,但是其提出了這個問題,並給出了規范。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM