Spring中Bean的作用域
作用域类型
singleton
单例模式。
使用
singleton
定义的 Bean 在 Spring 容器中只有一个实例,是 Bean 默认的作用域。prototype
原型模式
每次通过 Spring 容器获取
prototype
定义的 Bean 时,容器都将创建一个新的 Bean 实例。request
在一次 HTTP 请求中,容器会返回该 Bean 的同一个实例。而对不同的 HTTP 请求,会返回不同的实例,该作用域仅在当前 HTTP Request 内有效。
session
在一次 HTTP Session 中,容器会返回该 Bean 的同一个实例。而对不同的 HTTP 请求,会返回不同的实例,该作用域仅在当前 HTTP Session 内有效。
global session
在一个全局的 HTTP Session 中,容器会返回该 Bean 的同一个实例。该作用域仅在使用 portlet context 时有效。
Spring 单例Bean 的线程安全问题
问题原因
Spring 中的 bean 默认都是单例的。
无状态 Bean 不存在线程安全问题。
如果单例 Bean 是一个无状态 Bean,也就是线程中的操作不会对 Bean 的成员执行查询以外的操作,那么这个 Bean 就是线程安全的。 比如 SpringMVC 的 Controller、Sevice、Dao等,这些 Bean 大多是无状态的。
有状态 Bean 存在线程安全问题。
当 Bean对象中存在可变的成员变量,并且有线程会去修改这个变量,此时多线程操作该 Bean 就会出现线程安全问题。
解决办法
改变 Bean 的作用域为
prototype
;相当于每次请求都会创建一个新的 bean 实例,自然不会存在线程安全问题。
在类中定义一个
ThreadLocal
成员变量,将需要的可变成员变量保存在ThreadLocal
中(推荐的一种方式)。ThreadLocal
本质上使用了以空间换时间
的方式,为每个线程都提供一个独立的变量副本,从而保证了多线程之间访问变量时不会产生冲突。
练习例子
线程不安全
创建一个存在可变成员变量的测试类,默认的 Bean 是单例的,若是存在可变成员变量则是有状态 Bean,会产生线程安全问题。
@Component
public class DefaultSingleBean {
String msg = "默认消息";
}
创建一个API接口,开启两条线程获取 Bean,其中一条线程修改 Bean里面的变量,另一个线程只读线程里的可变成员变量。
- 若 Bean 是线程安全的,在只读的线程里获取可变成员变量时得到的是默认值。
- 若 Bean 是线程不安全的,在只读的线程里可能会读取到另一个线程修改后的值。
@GetMapping("/safeBean/default")
public void testThreadSafeBean() {
//修改值线程
new Thread(() -> {
//从ApplicationContext容器中获取
DefaultSingleBean defaultSingleBean = SpringBeanUtil.getBean(DefaultSingleBean.class);
defaultSingleBean.msg = "线程A信息";
System.out.println(defaultSingleBean.msg);
}).start();
//只读线程
new Thread(() -> {
//从ApplicationContext容器中获取
DefaultSingleBean defaultSingleBean = SpringBeanUtil.getBean(DefaultSingleBean.class);
System.out.println(defaultSingleBean.msg);
}).start();
}
//output
//线程A信息
//只读线程:线程A信息
线程安全
- 第一种方法:改变 Bean 的作用域为
prototype
;
/**
* 测试类-有状态的Bean
*/
@Component
@Scope("prototype")
public class BeanScopeSafeBean {
//默认消息
String msg = "默认消息";
}
创建一个API,测试修改Bean的作用域为prototype 来保证线程安全。
/**
* 第一种方法:测试修改Bean的作用域为prototype 来保证线程安全
*/
@GetMapping("/safeBean/beanScope")
public void testThreadSafeBeanScope() {
new Thread(() -> {
//从ApplicationContext容器中获取
BeanScopeSafeBean beanScopeSafeBean = SpringBeanUtil.getBean(BeanScopeSafeBean.class);
beanScopeSafeBean.msg = "线程A信息";
System.out.println(beanScopeSafeBean.msg);
}).start();
Thread.sleep(1000);
new Thread(() -> {
//从ApplicationContext容器中获取
BeanScopeSafeBean beanScopeSafeBean = SpringBeanUtil.getBean(BeanScopeSafeBean.class);
System.out.println(beanScopeSafeBean.msg);
}).start();
}
//output
//线程A信息
//默认消息
- 第二种方法:使用
ThreadLocal
封装成员变量;
@Component
public class ThreadLocalSafeBean {
/**
* 使用ThreadLocal保证可变变量线程安全
*/
ThreadLocal<String> threadLocalMsg = ThreadLocal.withInitial(()->"默认消息");
}
创建一个API,测试使用ThreadLocal解决线程安全的问题。
/**
* 第二种方式:测试使用ThreadLocal解决线程安全的问题
*/
@SneakyThrows
@GetMapping("/safeBean/threadlocal")
public void testThreadSafeBeanThreadLocal() {
new Thread(() -> {
//从ApplicationContext容器中获取
ThreadLocalSafeBean threadLocalSafeBean = SpringBeanUtil.getBean(ThreadLocalSafeBean.class);
threadLocalSafeBean.threadLocalMsg.set("线程A信息");
System.out.println(threadLocalSafeBean.threadLocalMsg.get());
}).start();
Thread.sleep(1000);
new Thread(() -> {
//从ApplicationContext容器中获取
ThreadLocalSafeBean threadLocalSafeBean = SpringBeanUtil.getBean(ThreadLocalSafeBean.class);
System.out.println(threadLocalSafeBean.threadLocalMsg.get());
}).start();
}
//output
//线程A信息
//默认消息