一、面试标准答案(30 秒杀手锏

"分情况看

**如果 Bean 是无状态的(如 Service/DAO),**Spring 不会做任何多线程封装,由开发者自行保证线程安全——但因为这些 Bean 没有可变状态(成员变量),实际上是线程安全的

如果 Bean 有可变状态(如 View Model/POJO 携带成员变量),开发者必须自行处理同步,Spring 不管。最常见的做法是把 scope 改成 prototype,让每次注入都是新实例。"

二、为什么 Spring 单例 Bean 默认是"实际线程安全"

2.1 三种情况分析
情况 是否线程安全 原因
无状态 Bean(Service/DAO) ✅ 实际安全 没有成员变量 / 成员变量是 final / 方法参数都是局部变量
有状态 Bean 但无成员变量修改 ✅ 实际安全 状态都在方法参数 / 局部变量里(线程栈隔离)
有状态 Bean 且修改成员变量 ❌ 不安全 多线程共享同一个实例,成员变量被并发改
2.2 为什么 Spring 不封装多线程?
原因 解释
1. 性能 加锁/synchronized 会让 Bean 性能降 10-100 倍
2. 与 Spring 设计哲学冲突 Spring 是"轻量级容器",不做重量级封装
3. 无意义 Service/DAO 本来就无状态,封装没意义
4. 复杂业务 有状态 Bean 的同步策略因业务而异,Spring 不知道该用哪种
2.3 Spring 的官方说法

"Spring 框架没有对单例 bean 进行任何多线程的封装处理。关于单例 bean 的线程安全和并发问题需要开发者自行去搞定。"

三、Java 内存模型(JMM)核心知识(面试加分

3.1 线程栈 vs 堆

线程栈(私有)                    堆(共享)
┌──────────────┐                 ┌──────────────┐
│ 局部变量      │                 │  对象实例     │
│ 方法参数      │                 │  成员变量     │
│ 临时计算结果  │                 │  静态变量     │
└──────────────┘                 └──────────────┘
     ↓                              ↓
 每个线程一份                    所有线程共享

关键点:

  • 局部变量 / 方法参数 = 线程私有(每个线程一份,天然安全)
  • 成员变量 / 静态变量 = 线程共享(多线程会并发修改,危险
3.2 无状态 Bean 为什么安全?

@Service  // 单例
public class UserService {
    
    // 没有成员变量!所有数据都在方法参数里
    public User findById(Long id) {  // id 是参数,线程私有
        return userDao.findById(id);
    }
    
    public void save(User user) {  // user 是参数,线程私有
        userDao.save(user);
    }
}

为什么安全?

  • 1000 个线程同时调用 findById(123L)
  • 每个线程有自己独立的 id 参数
  • 没有共享变量被修改
  • 天然线程安全

四、有状态 Bean 的 4 种处理方案

方案 1:scope = prototype(最常用

@Service
@Scope("prototype")  // 每次注入都是新实例
public class ShoppingCart {
    private List<Item> items = new ArrayList<>();  // 成员变量(可变)
    
    public void addItem(Item item) {
        items.add(item);
    }
}

缺点: 每次注入新对象,失去 Spring 单例的性能优势(创建/GC 成本高)。

方案 2:ThreadLocal(金融项目实战用得多

@Service
public class UserContext {
    // 每个线程独立一份,不互相干扰
    private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
    
    public static void set(User user) {
        currentUser.set(user);
    }
    
    public static User get() {
        return currentUser.get();
    }
    
    public static void clear() {
        currentUser.remove();  // 必须清理!防止内存泄漏
    }
}

金融项目实战: Spring Security 的 SecurityContextHolder、日志 MDC(traceId 追踪)都是 ThreadLocal 实现的。

方案 3:加锁 / synchronized(金融项目最后手段

@Service
public class CounterService {
    private int count = 0;  // 共享变量
    
    public synchronized void increment() {  // 锁
        count++;
    }
}

缺点: 性能差,不推荐

方案 4:使用 ConcurrentHashMap / AtomicInteger推荐

@Service
public class CounterService {
    private final AtomicInteger count = new AtomicInteger(0);  // CAS 无锁
    
    public void increment() {
        count.incrementAndGet();  // 原子操作,无需加锁
    }
}

优点: 性能高 + 线程安全,金融项目首选(计数器、累加器、统计指标)。

五、面试官追问应对

追问 1:Spring Bean 默认是单例还是多例?

"默认 singleton(单例),容器启动时创建一次,整个应用共享。

还有 5 种 scope:prototype(多例)/ request(一次请求一个)/ session(一次会话一个)/ application(一次应用一个)/ websocket(一次 WebSocket 一个)。"

追问 2:单例 Bean 怎么变成多例?

"单例 Bean 变多例有 4 种写法

1.简单场景@Scope("prototype")

2.类型安全@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)

3.工厂方法@Bean + @Scope

4.注入到单例要新实例@Scope + proxyMode = TARGET_CLASS@Lazy

    关键坑:prototype 注入到 singleton,默认不是真多例,必须用 proxyMode 或 @Lazy。"

    追问 3:Spring 怎么处理有状态 Bean?

    "不处理。Spring 把多线程安全问题完全交给开发者。如果 Bean 有可变成员变量,开发者需要:

    1.用 ThreadLocal 隔离

    2.用 synchronized / ReentrantLock 同步

    3.用 AtomicInteger / ConcurrentHashMap 无锁并发

    4.改 scope = prototype

      报表计数用 AtomicInteger,日志 traceId 用 ThreadLocal,效果都很好。"

      追问 4:Controller 是单例吗?会有线程安全问题吗?

      "Controller 默认单例。但因为 Controller 是无状态的(成员变量基本是 Service 引用,Service 本身也是单例无状态),实际线程安全

      如果你在 Controller 里直接用成员变量存请求数据(如 private User currentUser),会线程不安全——多个请求会覆盖。

      正确做法:参数传递,或用 ThreadLocal 存当前请求的用户信息。"

      追问 5:Spring 的 @Transactional 事务方法线程安全吗?

      "不保证。Spring 事务是用 ThreadLocal 存 Connection 的(保证同一个事务用同一个连接)。

      但 @Transactional 方法本身不是线程安全保证。如果在事务方法里修改成员变量,照样不安全。事务只保证 ACID,不保证并发安全。"

      六、一句话总结

      "Spring 单例 Bean 默认线程安全,前提是无状态。Service/DAO 没有成员变量,每个线程有自己的方法参数(线程栈隔离),所以安全。

      有状态 Bean(如 View Model 带成员变量)不安全,要用 ThreadLocal / AtomicXxx / synchronized / scope=prototype 处理。

      Spring 不会帮你处理多线程,由开发者自行保证。"

      七、记忆口诀

      "Spring 单例 + 无状态 = 安全,Spring 单例 + 有状态 = 不安全"

      "Service/DAO 不用管,View Model 自己管"

      "ThreadLocal 隔离,AtomicXxx 计数,synchronized 兜底,prototype 多例"

      "局部变量是线程私有的,成员变量是线程共享的"

      Logo

      汇聚全球AI编程工具,助力开发者即刻编程。

      更多推荐