前言
首先我们解释一下什么是单例,什么是多例?
- 单例,就是一个类在内存当中只存在一个实例(对象),在任意位置使用这个对象,都是同一个对象。
- 多例,就是一个类在内存当中存在多个实例(对象),每一次使用这个对象,都是通过构造器来创建对象,是不同的对象。
代码验证 Controller 是单例还是多例
我们通过代码来验证 Spring Controller 是不是线程安全的,代码思路如下:
- 我们创建一个 Controller 类,然后编写其空参构造方法 ,再编写一个 映射请求的方法(get请求)
项目是使用 SpringBoot 快速构建的,所以不需要配置繁琐的 XML 就能搭建 Web 环境。
@RestController
public class CategoryController {
public CategoryController(){
System.out.println("CategoryController创建了。。。");
}
@GetMapping("/is")
public String is(){
System.out.println(this);
return "is";
}
}
- 上面代码在构造方法打印一句话,如果 Controller 是单例的话,不管前端发送多少个请求,构造方法只会执行一次。
使用 PostMan 发送请求,不管发送几次请求这个 Controller ,构造方法只会执行一次,而且打印 this(Controller对象)也都是同一个地址,说明 Controller 默认是单例的。 - 如果想把 Controller 改为多例对象,该怎么操作?
其实也很简单,就是加一个注解 @Scope(“prototype”) 就可以搞定,下面代码演示:
@RestController
@Scope("prototype")
public class CategoryController {
public CategoryController(){
System.out.println("CategoryController创建了。。。");
}
@GetMapping("/is")
public String is(){
System.out.println(this);
return "is";
}
}
- 如下图所示,每一次请求 Controller 都会重新创建一个,打印 this 也是不同的地址,说明我们成功修改 Controller 作用域了
Controller 线程安不安全?
1、什么时候会出现线程不安全?
- 首先要想线程不安全,肯定系统环境处于多线程高并发环境下,而且这多个线程需要同时操作一个共享变量,而且还能同时修改这个变量的值,这样才会出现线程不安全的情况。
2、单例的 Controller 是否线程安全?
- 我们写一段代码验证线程不安全的情况,我们还是让 Controller 处于单例状态,然后下面定义一个全局变量,编写两个映射请求的方法,使用 PostMan 依次访问这两个接口方法
@RestController
//@Scope("prototype")
public class CategoryController {
private Integer accessCount = 0;
public CategoryController(){
System.out.println("CategoryController创建了。。。");
}
@GetMapping("one")
public Integer one(){
System.out.println(++accessCount);
return accessCount;
}
@GetMapping("two")
public Integer two(){
System.out.println(++accessCount);
return accessCount;
}
@GetMapping("/is")
public String is(){
System.out.println(this);
return "is";
}
}
- 我们先访问了 one 接口,accessCount 值变为了 1,然后再访问 ** two 接口**,accessCount 值变为了 2,这说明,在单例的情况下,Controller 是不安全的,所以尽量避免定义全局变量。
3、多例的 Controller 是否线程安全?
- 上面我们知道了单例的 Controller 是线程不安全的,接下来我们演示多例的 Controller 是否线程安全,如下代码
@RestController
@Scope("prototype")
public class CategoryController {
private Integer accessCount = 0;
public CategoryController(){
System.out.println("CategoryController创建了。。。");
}
@GetMapping("one")
public Integer one(){
System.out.println(++accessCount);
return accessCount;
}
@GetMapping("two")
public Integer two(){
System.out.println(++accessCount);
return accessCount;
}
@GetMapping("/is")
public String is(){
System.out.println(this);
return "is";
}
}
- 如下图,说明了在多例情况下,Controller 是线程安全的。
如何保证单例的 Controller 是线程安全的?
我们可以通过下面措施来保证单例的 Controller 是线程安全的:
- 不要在 Controller 定义全局共享变量
- 可以使用 ThreadLocal 变量来维护每一个请求(每一个请求都是一个开启新线程)需要使用的全局变量
- 如果必须在 Controller 定义全局变量,可以将 Controller 改为多例的,也就是添加 **@Scope(“prototype”)**注解
总结
- 其实 Controller 也是 Spring Bean其中一种,我们可以类比一下,就是所有被 Spring 管理的 Bean 默认都是单例对象,都有可能出现线程安全的问题,解决方案也是和上面相同的。
- 单例下的 Controller 在项目启动的时候,就会初始化;多例下的 Controller 只有在访问的时候才会初始化。
- 我们列举一下 Spring Bean 的几大作用域
singleton
:单例模式,当 Spring 创建 ApplicationContext 容器的时候,Spring会欲初始化所有的该作用域实例,加上 lazy-init就可以避免预处理。prototype
:原型模式,每次通过 getBean 获取该 Bean 就会新产生一个实例,这种状态的 Bean 还是会被 Spring 管理,只不过不会缓存了,用完就销毁了。request
:就是每次请求都新产生一个实例session
:在一次会话之内都是同一个实例global session
:全局的 Web 域,类似于 Servlet 的 ServletContext。