1 前言
在笔试面试的时候经常会遇到的一个问题:Spring 的 Controller 是单例还是多例?
首先答案是:controller默认是单例的,不要使用非静态的成员变量,否则会发生数据逻辑混乱。而且正因为单例,所以它也不是线程安全的。
那么既然不是线程安全的,那么spring怎么保证做到并发的安全性呢?
2 正文
首先我们来看下面的例子:
package com.springboot.springbootdemo.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ScopeController { public int number = 0; @RequestMapping("/text1") public int test1(){ number=number+1; return number; } @RequestMapping("/text2") public int test2(){ number=number+1; return ++number; } } 复制代码
可以发现我们首先访问 http://localhost:8080/text1,得到的答案是1;然后我们再访问http://localhost:8080/text2,得到的答案是 2。所以得到的不同的值,证明这是线程不安全的。
说到这个问题,我们就需要说到之前提到的一个问题,那就是关于spring的bean作用域,关于spring的作用域可以参考之前的一篇文章:传送门Spring注解(三):@scope设置组件作用域。
在Spring注解开发中@Scope注解可以用于设置组件的作用域,通过@Scope源码,可以发现@Scope注解有五种作用域,即:
SINGLETON:单例模式,默认模式,不写的时候默认是SINGLETON PROTOTYPE:原型模式 REQUEST:同一次请求则只创建一次实例 SESSION:同一个session只创建一次实例 GLOBAL SESSION:全局的web域,类似于servlet中的application。 复制代码
在默认的情况下,controller是单例模式singleton,单例是不安全的,因为它会导致属性重复使用。
接下来我们再来给controller增加作用多例 @Scope("prototype")
package com.springboot.springbootdemo.controller; import org.springframework.context.annotation.Scope; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @Scope("prototype") public class ScopeController { public int number = 0; @RequestMapping("/text1") public String test1(){ number=number+1; return String.valueOf(number); } @RequestMapping("/text2") public String test2(){ number=number+1; return String.valueOf(number); } } 复制代码
可以发现我们首先访问 http://localhost:8080/text1,得到的答案是1;然后我们再访问http://localhost:8080/text2,得到的答案是 1。这时候就是线程安全的了。
3 总结
通过上面的例子,可以得出结论:
1、spring的controller默认是单例模式,而这种模式下是线程不安全的,所以在这种模式下不要在controller种定义成员变量;
2、在单例模式下可以通过使用ThreadLocal 解决线程安全问题。
线程安全问题主要是全局变量和静态变量引起的。若每个线程中对全局变量、静态变量读操作,而无写操作,一般来说这个全局变量是线程安全的。若多个线程同时执行写操作,需要考虑线程同步问题,否则影响线程安全。spring 使用ThreadLocal 实现高并发下 共享资源的同步。
使用ThreadLocal 为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线 程都完全拥有该变量。
ThreadLocal 为每一个变量维护变量的副本的原理如下:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } 复制代码
3、通过@Scope(“prototype”)注解,将默认的作用域改为多例模式,这时候就能够在controller种定义非静态的成员变量了。
在spring的controller默认是单例,原因有两点:
(1)为了性能:单例不用每次都创建
(2)不需要多例:只要controller中不定义属性,那么单例完全是安全可用的,如果定义了,那单例肯定会出现竞争访问;非要定义,则通过注解@Scope("prototype"),将其设置为多例模式。