Spring单例Bean和线程安全 - duanxz - 博客园
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解决线程安全问题
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 of Process 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:
- accessing global variables or the heap
- allocating/reallocating/freeing resources that have global scope ( files, sub- processes, pipes, etc.)
- indirect accesses through handles or pointers
- any visible side-effect (e.g., access to volatile variables in the C programming language)
Implementation
There are a few ways to achieve thread safety:
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 raceconditions, deadlocks, livelocksand starvation.
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.
Shared data areaccessed by using atomic operations which cannot be interrupted by otherthreads. This usually requires using special machinelanguage 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也没有从本质上解决这个问题,但是其提出了这个问题,并给出了规范。