Spring Bean 作用域、线程安全与生命周期
面试里问“Spring 中的单例 Bean 是线程安全的吗”真正考的不是背一句“不是线程安全”而是你能不能把 Bean 的作用域、对象状态、Spring 容器创建过程串起来。一句话先给结论Spring 默认把 Bean 做成单例但单例只代表容器里只有一个对象不代表这个对象一定线程安全。是否线程安全取决于这个 Bean 里面有没有被多个线程共享并修改的状态。1. Bean 默认是不是单例Spring 常见的 Bean 作用域里最常用的是singleton和prototype。作用域含义常见场景singleton一个 Spring IoC 容器中只有一个 Bean 实例Controller、Service、DAOprototype每次获取 Bean 都创建一个新实例有独立状态、短生命周期对象默认情况下Spring 管理的 Bean 是singleton。ServiceScope(singleton)publicclassUserServiceImplimplementsUserService{}这段代码里的UserServiceImpl在同一个 Spring 容器里只会有一个实例。多个请求进来时访问的是同一个 Service 对象。2. 单例为什么不等于线程安全Web 项目里一个请求通常由一个线程处理。假设 Controller 里放了一个可变成员变量RestControllerRequestMapping(/user)publicclassUserController{privateintcount;AutowiredprivateUserServiceuserService;GetMapping(/getById/{id})publicUsergetById(PathVariableIntegerid){count;System.out.println(count);returnuserService.getById(id);}}count是 Controller 对象的成员变量而 Controller 默认是单例。多个请求线程同时执行count时就会竞争同一个变量。这就是线程不安全的来源可变成员变量被多个线程共享修改。反过来平时我们写的 Service 和 DAO 大多是无状态的ServicepublicclassUserService{AutowiredprivateUserMapperuserMapper;publicUsergetById(Integerid){returnuserMapper.selectById(id);}}这里没有在 Bean 自己身上保存请求级数据。id是方法局部变量每个线程有自己的栈帧所以没有共享状态。这样的单例 Bean 在通常业务下可以认为是线程安全的。3. 真的需要状态怎么办如果 Bean 中确实有会被修改的成员变量常见处理方式有四种方案适合场景注意点改成局部变量请求临时数据、计算中间值最推荐简单稳定使用无状态设计Service、DAO、Controller后端业务类的默认选择加锁或使用并发容器全局计数器、共享缓存要评估性能和锁粒度改成 prototype对象必须携带独立状态Web 层并不常用注入方式也要注意面试回答时不要只说“用多例解决”。大多数业务类更好的方案是不要把请求状态放到单例 Bean 的成员变量里。4. Bean 生命周期怎么走Spring 创建 Bean 并不是new一个对象这么简单。容器会先把配置或注解解析成BeanDefinition再根据定义信息完成实例化、依赖注入、初始化、代理增强和销毁。可以把生命周期拆成 8 步读取配置或注解生成BeanDefinition。调用构造方法实例化 Bean。给 Bean 做依赖注入也就是给属性赋值。处理 Aware 接口例如BeanNameAware、BeanFactoryAware、ApplicationContextAware。执行BeanPostProcessor的前置处理。执行初始化方法例如InitializingBean#afterPropertiesSet或自定义init-method。执行BeanPostProcessor的后置处理这一步可能产生 AOP 代理对象。容器关闭时销毁 Bean。BeanDefinition可以理解成 Bean 的“图纸”。里面会记录类名、作用域、是否懒加载、初始化方法、属性值等信息。beaniduserServiceclasscom.example.UserServiceImplscopesingletonlazy-inittruepropertynameuserDaorefuserDao//beanSpring 不是直接凭空创建对象而是先把这些信息封装起来再按生命周期流程创建 Bean。5. 面试回答模板可以这样回答Spring 中的 Bean 默认是单例的也就是同一个 IoC 容器中只有一个实例。但单例 Bean 本身不保证线程安全关键看 Bean 里有没有可变共享状态。一般 Controller、Service、DAO 都是无状态对象请求数据放在方法参数和局部变量里所以通常没有线程安全问题。如果在单例 Bean 中定义了会被多个线程修改的成员变量就要考虑线程安全可以改成局部变量、无状态设计、加锁或者在特殊场景下使用多例。如果继续问生命周期可以补一句Spring 会先解析配置生成BeanDefinition然后实例化 Bean、做依赖注入、处理 Aware 接口、执行BeanPostProcessor前置、执行初始化方法、执行BeanPostProcessor后置最后在容器关闭时销毁 Bean。AOP 代理通常发生在后置处理阶段。6. 小结Spring Bean 线程安全问题的核心不是singleton这个词而是对象状态。只要记住这条线就够了默认单例 → 多线程共享同一个 Bean → 无状态通常安全 → 有可变成员变量就要处理并发问题 → 生命周期里后置处理可能生成代理对象。