题目
单例 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 变量都是当前线程所绑定的。

> Spring 中事务底层 `TransactionSynchronizationManager` 就大量使用了 ThreadLocal
>
> ```java
> @RestController
> public class TestController {
> private int var = 0; // 定义一个普通变量
> ThreadLocal<Integer> var = new ThreadLocal<>(); // 用 ThreadLocal 来封装变量
> ```
