题目
单例 Bean 的线程安全问题及解决方案
答案
@[toc] --- ## 单例 Bean 的线程安全问题 先来解释下线程安全和**无状态**、**有状态**: - 所谓无状态就是不会保存数据(不存在写数据,当然也就不存在线程安全问题了) - 而有状态就是有数据存储功能(就是除了读数据它还写数据了),可以这样讲,线程安全问题都是由全局变量及静态变量引起的。换句话说,**若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的**),比如 controller 中存着一个可变的全局成员变量,如下例: ```java @RestController public class TestController { // 一个非线程安全变量(全局变量) private int var = 0; @GetMapping(value = "/test_var") public String test() { var ++; System.out.println("普通变量 var:" + var); return "普通变量 var:" + var ; } } ``` 当多个用户同时请求一个服务器时,tomcat 会给每一个请求分配一个线程,这时多个线程会并发执行该请求所对应的业务逻辑(controller里的方法) 在 postman 里面发三次请求,结果如下: ``` 普通变量 var:1 普通变量 var:2 普通变量 var:3 ``` 说明他不是线程安全的。 而 Controller、Service 和 Dao 层本身因为可以**携带状态**所以并不是线程安全的!!!只不过我们实际开发中很少去定义这种非线程安全的变量罢了(也就是我们常说的大部分 Bean 其实都是无状态的,所以在某种程度上来说 Bean 其实是安全的)。 Spring 中的 Bean 默认是单例模式的,框架并没有对 Bean 进行多线程的封装处理,那么,如果遇到这种情况,该怎么办呢? ## 解决方案 1)**更换 Bean 的作用域为原型(多例)模式 `@scope("prototype")`**,这样每次请求 Bean 就相当于是 new Bean() 这样就可以保证线程的安全了 > 还是上面那个例子: > > ```java > @RestController > @Scope(value = "prototype") > public class TestController { > > // 一个非线程安全变量(全局变量) > private int var = 0; > > @GetMapping(value = "/test_var") > public String test() { > var ++; > System.out.println("普通变量 var:" + var); > return "普通变量 var:" + var ; > } > } > ``` > > 这样一来,每个请求都单独创建一个Controller容器,所以各个请求之间是线程安全的,三次请求结果: > > ``` > 普通变量 var:1 > 普通变量 var:1 > 普通变量 var:1 > ``` > > 那么,多实例 Bean 是不是一定就线程安全呢? > > ```java > @RestController > @Scope(value = "prototype") > public class TestController { > private int var = 0; > private static int staticVar = 0; > > @GetMapping(value = "/test_var") > public String test() { > var ++; > staticVar ++; > System.out.println("普通变量 var:" + var + ",静态变量 staticVar:" + staticVar); > return "普通变量 var:" + var + "静态变量 staticVar:" + staticVar; > } > } > ``` > > 看三次请求结果: > > ``` > 普通变量 var:1,静态变量 staticVar:1 > 普通变量 var:1,静态变量 staticVar:2 > 普通变量 var:1,静态变量 staticVar:3 > ``` > > **静态变量始终是只有一份的,即便是多例 Bean,也是非线程安全的** > > **所以,尽量不要在 Controller/Service 等容器中定义静态变量,不论是单例还是多例,他都是线程不安全的** 2)**使用 ThreadLocal** 可以将一些非线程安全的变量以 ThreadLocal 存放,在同一次请求响应的调用线程中,所有对象所访问的同一 ThreadLocal 变量都是当前线程所绑定的。 ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/image-20220708153912436.png) > Spring 中事务底层 `TransactionSynchronizationManager` 就大量使用了 ThreadLocal > > ```java > @RestController > public class TestController { > private int var = 0; // 定义一个普通变量 > ThreadLocal<Integer> var = new ThreadLocal<>(); // 用 ThreadLocal 来封装变量 > ```