Spring 的 bean默认是单例的,在高并发下,如果在 Spring 的单例 bean 中设置成员变量,则会发生并发问题。最近在进行开发时,错误的在单例的bean中使用了成员变量,导致多个线程大并发访问时,出现赋值错误及日志打印混乱的问题。
本文就对单例 bean 及多线程安全的问题做一次较为深入的探讨,也是对自我的一次反省,之后的开发中,杜绝此类问题,修正开发习惯。
首先我们回顾一下单例模式的概念。单例模式的意思是只有一个实例,例如在Spring容器中某一个类只有一个实例,而且自行实例化后并项整个系统提供这个实例,这个类称为单例类。
当多个用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑(成员方法),此时就要注意了,如果该处理逻辑中有对单例状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。
一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如 RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder 等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
简单的说,若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;
若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
更进一步划分:
常量始终是线程安全的,因为只存在读操作。 每次调用方法前都新建一个实例是线程安全的,因为没有访问共享的资源。 局部变量是线程安全的。因为每执行一个方法,都会在独立的空间创建局部变量,它不是共享的资源。局部变量包括方法的参数变量和方法内变量。这也就是我们常说的方法封闭。 如果实例无状态,则是线程安全的。如果实例中存在对同一个值的不同的操作行为,或者值在不同线程中都会变,那么就需要注意,不要使用成员变量存储属性。
这里我们引入无状态bean和有状态bean。
有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。
无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 .不能保存数据,是不变类,是线程安全的。
在spring中无状态的Bean适合用不变模式,就是单例模式,这样可以共享实例提高性能。
有状态的Bean在多线程环境下不安全,适合用 Prototype 原型模式。
Prototype: 每次对 bean 的请求都会创建一个新的 bean 实例。
struts2每次处理一个请求,struts2就会实例化一个对象,这样就不会有线程安全的问题了。Struts2 是线程安全的,当然前提情况是,Action 不交给 spring管理,并且不设置为单例。
请求时多线程请求的,但是每次请求过来调用的Controller对象都是一个,而不是一个请求过来就创建一个controller对象
原因就在于如果这个controller对象是单例的,那么如果不小心在类中定义了类变量,那么这个类变量是被所有请求共享的,
这可能会造成多个请求修改该变量的值,出现与预期结果不符合的异常
在单例的情况下 相当于所有类变量对于每次请求都是共享的,每一次请求对类变量的修改都是有效的
那有没有办法让controller不以单例而以每次请求都重新创建的形式存在呢?答案是当然可以,只需要在类上添加注解@Scope("prototype")即可,这样每次请求调用的类都是重新生成的(每次生成会影响效率)还有其他方法么?
答案是肯定的!使用ThreadLocal 来保存类变量,将类变量保存在线程的变量域中,让不同的请求隔离开来.
注:servlet Struts1 SpringMvc 是线程不安全的,当然如果你不使用实例变量也就不存在线程安全的问题了。
为避免发生线程安全问题,在开发和设计系统的时候注意下一下三点:
自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明 对线程环境下,对每一个共享的可变变量都要注意其线程安全性 我们的类和方法在做设计的时候,要尽量设计成无状态的