Spring 中的 Bean 是线程安全的吗?

#Java#面试八股

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(); }   // 线程池复用线程,不清就串数据/泄漏
}

四、容易混淆的三点

  1. Controller 默认安全吗? 只要不写成员变量就是安全的,因为请求参数都是方法参数(栈上私有)。危险来自"顺手加个成员缓存"。
  2. @Scope("prototype") 能解决吗? 只在"每次注入都该拿到独立实例"时有效;它不保证对象内部的线程安全,也无法解决单例注入 prototype 后仍长期持有同一实例的问题。
  3. Spring 自己哪些组件是线程安全的? ApplicationContext、JdbcTemplate、RestTemplate(默认配置下)都是设计为可并发使用的;而 SimpleDateFormat、HttpServletRequest、SqlSession(非线程安全)不能跨线程共享。

五、常见追问

  • 问:synchronized 加在 Controller 方法上可行吗? 可行但代价大:锁住的是单例对象,等于把接口串行化;只适合极低频的管理端点。
  • 问:为什么 ThreadLocal 要配合拦截器清理? Tomcat 线程池复用线程,上一次请求留下的值会被下一次请求读到,同时造成 key 弱引用被回收后的 value 泄漏。
  • 问:@Async 方法里拿 RequestContextHolder 为什么为空? 请求上下文存在原线程的 ThreadLocal 中,异步线程看不到,需要显式传递或配置 TaskDecorator。

小结

判断 Bean 线程安全只需两问:这个 Bean 是单例吗?它有没有被多个线程写的可变字段?两问都"是"就要处理——无状态化、原子类、ThreadLocal(配 remove)、或改为局部变量。Spring 只负责"造对象",不负责"护并发"。

笔记加载中…