基于本文回答
0
评论

Spring MVC 默认是单例还是多例?是否存在线程安全问题?

知识点图片

这是一个非常经典且高频的面试题。直接给出结论:

  1. Spring MVC 默认是单例(Singleton)的。
  2. 是的,存在线程安全问题。

下面详细解释为什么,以及如何解决。


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),或者只使用方法局部变量,就能天然避免线程安全问题。做到“无状态”,即可享受单例的高性能,又无需担心线程安全。
右滑查看面试常问