Spring MVC 默认是单例还是多例?是否存在线程安全问题?
这是一个非常经典且高频的面试题。直接给出结论:
- Spring MVC 默认是单例(Singleton)的。
- 是的,存在线程安全问题。
下面详细解释为什么,以及如何解决。
1. 为什么默认是单例?
在 Spring 容器中,Bean 的作用域(Scope)默认就是 singleton。对于 Spring MVC 的 Controller 而言,默认也是单例。
原因主要有两点:
- 性能优势: 单例模式下,Controller 实例在容器启动时初始化一次。后续的所有请求都由这同一个实例处理,避免了每次请求都创建和销毁对象的开销(内存分配、垃圾回收等),大大提高了性能。
- 资源节约: 减少了内存占用,特别是在高并发场景下,如果每个请求都创建一个 Controller,内存压力会非常大。
2. 为什么存在线程安全问题?
因为 Controller 是单例的,这意味着所有的 HTTP 请求都会并发地访问同一个 Controller 对象。
- Web 容器(如 Tomcat)是多线程的,每一个请求都会分配一个独立的线程。
- 如果多个线程同时调用同一个 Controller 实例的方法,并且该 Controller 中包含可变的成员变量(Stateful),就会发生资源竞争,导致数据错乱。
举个“不安全”的例子:
java
@RestController
public class UnsafeController {
// 这是一个成员变量,它是非线程安全的!
private int count = 0;
@GetMapping("/add")
public String add() {
// 多个线程同时执行这行代码,会出现并发问题
count++;
return "Count: " + count;
}
}
在这个例子中,如果两个用户几乎同时访问 /add,他们可能看到相同的数字,或者数字跳跃,这就是典型的线程安全问题。
3. 如何解决线程安全问题?
虽然存在风险,但在实际开发中,我们很少遇到 Controller 的线程安全问题,因为我们通常遵循了最佳实践。以下是几种解决方案:
方案一:保持 Controller “无状态”(最佳实践)
这是最常用、最推荐的做法。不要在 Controller 中定义可变的成员变量。
- 可以定义什么? 可以定义 Service、DAO 等依赖对象的引用。因为这些 Service/DAO 通常本身也是单例且无状态的(它们只包含方法逻辑,不保存用户数据),所以是安全的。
- 数据放在哪里? 将请求参数、业务数据定义为方法内的局部变量。局部变量存储在栈帧中,每个线程独享一份,天然线程安全。
安全的代码示例:
java
@RestController
public class SafeController {
// Service 是无状态的单例,线程安全
@Autowired
private UserService userService;
@GetMapping("/user")
public String getUser(@RequestParam String name) {
// count 是局部变量,每个线程都有自己的一份,互不干扰
int count = 0;
count++;
return userService.findByName(name);
}
}
方案二:使用 ThreadLocal
如果你确实需要在 Controller 的不同方法间共享数据,或者需要存储请求级别的上下文信息(如当前登录用户信息),可以使用 ThreadLocal。
ThreadLocal 为每个线程提供独立的变量副本,隔离了多个线程对数据的访问。Spring MVC 底层的 RequestContextHolder 就是通过 ThreadLocal 实现的。
java
private ThreadLocal<Integer> countHolder = new ThreadLocal<>();
方案三:改为多例模式(Prototype)(不推荐)
你可以通过注解强制让 Spring 为每个请求创建一个新的 Controller 实例。
java
@RestController
@Scope("prototype") // 或者 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class PrototypeController {
private int count = 0; // 此时每个请求都有自己的 count,安全
// ...
}
缺点: 性能极差。每次请求都要创建对象、依赖注入、销毁对象,给 GC 带来巨大压力。除非万不得已,不要这么做。
方案四:使用并发类或同步锁(视情况而定)
如果必须有共享变量(例如全局计数器),可以使用 Java 并发包中的类,如 AtomicInteger,或者使用 synchronized 关键字。
AtomicInteger:性能较好,适合简单计数。synchronized:会阻塞线程,导致吞吐量大幅下降,Web 开发中应尽量避免在 Controller 方法上加锁。
总结
- 默认单例: Spring MVC Controller 默认是单例的。
- 线程不安全: 如果你在 Controller 中定义了可变的成员变量,就是线程不安全的。
- 最佳实践: 只要不在 Controller 中定义成员变量(除了注入的 Service/DAO),或者只使用方法局部变量,就能天然避免线程安全问题。做到“无状态”,即可享受单例的高性能,又无需担心线程安全。