Spring 中的 Bean 是线程安全的吗?
结论先行:Spring 容器本身不为 Bean 的线程安全兜底,它只保证"一个单例 Bean 只有一个实例"这件事,而"多个线程同时用这个实例是否出错"取决于 Bean 自己有没有可变共享状态。无状态 Bean(只有方法、没有可写字段)天然安全;带可变成员变量的单例 Bean 一定不安全;哪怕换成 prototype 作用域,也只是换了实例,并没有换掉"同一个对象被多线程共享"这个前提。
一、先看共享关系
请求线程 T1 ─┐
请求线程 T2 ─┼──▶ 同一个单例 Bean 实例 ──▶ 可写字段 count / SimpleDateFormat
请求线程 T3 ─┘ ▲
└─ 竞态发生在这里
容器在启动时把单例 Bean 放进 singletonObjects,之后每次注入拿到的都是同一个引用。因此线程安全问题与"Spring 管不管"无关,只与"这个对象有没有并发写"有关。
二、按状态与作用域判断
| Bean 形态 | 是否存在共享可变状态 | 结论 |
|---|---|---|
| 只有常量和无状态方法(Service 常规写法) | 否 | 安全 |
| 单例 + 成员变量做缓存/计数 | 是 | 不安全,需原子类或锁 |
单例 + SimpleDateFormat 等非线程安全工具 | 是 | 不安全,换 DateTimeFormatter 或每次新建 |
单例 + ThreadLocal 保存上下文 | 线程隔离 | 安全,但要记得 remove() |
| prototype 作用域 | 每次注入新实例 | 实例间不共享,但同一实例若被多线程持有仍不安全 |
request/session 作用域 | 每请求/会话一个实例 | 请求内单线程时安全,异步线程内共享仍不安全 |
三、错误与正确写法对照
// 反例:单例 Controller 里放可变成员变量
@RestController
public class BadController {
private int visitCount = 0; // 所有请求共享,i++ 非原子
@GetMapping("/bad")
public int bad() {
return ++visitCount; // 并发下丢更新
}
}
@RestController
public class GoodController {
private final AtomicLong visitCount = new AtomicLong(); // 原子类保证可见性与原子性
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@GetMapping("/good")
public String good(@RequestParam String userId) {
visitCount.incrementAndGet();
// 方法内局部变量天生线程私有,随调用栈存在
String today = LocalDate.now().format(FMT); // 不可变,天然安全
return userId + "@" + today;
}
}
// 需要"每线程一份"时用 ThreadLocal,并务必在 finally 里清理
public class UserContext {
private static final ThreadLocal<String> HOLDER = new ThreadLocal<>();
public static void set(String userId) { HOLDER.set(userId); }
public static String get() { return HOLDER.get(); }
public static void clear() { HOLDER.remove(); } // 线程池复用线程,不清就串数据/泄漏
}
四、容易混淆的三点
- Controller 默认安全吗? 只要不写成员变量就是安全的,因为请求参数都是方法参数(栈上私有)。危险来自"顺手加个成员缓存"。
@Scope("prototype")能解决吗? 只在"每次注入都该拿到独立实例"时有效;它不保证对象内部的线程安全,也无法解决单例注入 prototype 后仍长期持有同一实例的问题。- Spring 自己哪些组件是线程安全的?
ApplicationContext、JdbcTemplate、RestTemplate(默认配置下)都是设计为可并发使用的;而SimpleDateFormat、HttpServletRequest、SqlSession(非线程安全)不能跨线程共享。
五、常见追问
- 问:
synchronized加在 Controller 方法上可行吗? 可行但代价大:锁住的是单例对象,等于把接口串行化;只适合极低频的管理端点。 - 问:为什么
ThreadLocal要配合拦截器清理? Tomcat 线程池复用线程,上一次请求留下的值会被下一次请求读到,同时造成 key 弱引用被回收后的 value 泄漏。 - 问:
@Async方法里拿RequestContextHolder为什么为空? 请求上下文存在原线程的ThreadLocal中,异步线程看不到,需要显式传递或配置TaskDecorator。
小结
判断 Bean 线程安全只需两问:这个 Bean 是单例吗?它有没有被多个线程写的可变字段?两问都"是"就要处理——无状态化、原子类、ThreadLocal(配 remove)、或改为局部变量。Spring 只负责"造对象",不负责"护并发"。