一、面试标准答案(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 多例"
"局部变量是线程私有的,成员变量是线程共享的"