【大白话说Java面试题 第153题】【06_Spring篇】第13题:Spring 中 Bean 是线程安全的吗?
2026/7/5 1:51:19 网站建设 项目流程

📌PDF:大白话说Java面试题 — 06_Spring篇

第13题:Spring 中 Bean 是线程安全的吗?

📚回答:

  • 核心考点: Spring Bean 的线程安全性是并发编程与 Spring 框架交叉的经典问题,大厂面试不会只问"是否安全",而是深入考察Spring 作用域与线程安全的关系singleton/prototype/request/session)、有状态 Bean vs 无状态 Bean 的设计原则ThreadLocal 在 Spring 中的正确使用姿势(内存泄漏风险)、以及@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)解决作用域代理问题的原理。面试官真正想判断的是:你是否能从框架设计层面理解线程安全的本质,以及能否在 Controller 层、Service 层、DAO 层等不同层级做出正确的线程安全设计。

1. Spring Bean 作用域与线程安全性

Spring 定义了 6 种 Bean 作用域,其中 4 种在 Web 环境下可用:

作用域说明线程安全性适用场景
singleton默认,每个 Spring 容器只有一个实例不安全(有状态时)无状态 Service、DAO、工具类
prototype每次获取都创建新实例安全(天然隔离)有状态对象,但创建开销大
request每个 HTTP 请求一个实例安全(请求隔离)Web 环境,请求级状态
session每个 HTTP Session 一个实例安全(会话隔离)Web 环境,用户级状态
application每个 ServletContext 一个实例不安全(有状态时)全局配置、缓存
websocket每个 WebSocket 连接一个实例安全(连接隔离)WebSocket 场景

关键结论:Spring 的singleton作用域本身不提供线程安全保证,线程安全取决于 Bean 的状态设计


2. 有状态 Bean vs 无状态 Bean——设计的分水岭
  • 2.1 无状态 Bean(线程安全)

    无状态 Bean 是指不保存任何实例变量的 Bean,所有操作都通过方法参数和返回值完成:

    @ServicepublicclassUserService{@AutowiredprivateUserDaouserDao;// 依赖注入,本身无状态publicUsergetUser(Longid){returnuserDao.findById(id);// 纯查询,不修改实例变量}publicvoidupdateUser(Useruser){userDao.update(user);// 操作通过参数传递,无实例变量修改}}

    无状态 Bean 的特征

    • 没有可变的实例变量(final常量除外);
    • 不保存用户会话信息或请求上下文;
    • 方法之间不共享状态;
    • 天然线程安全,所有线程共享同一个实例无风险。

    Spring 中 99% 的 Bean 应该是无状态的:Service、DAO、Mapper、Repository 等通常都是无状态设计。

  • 2.2 有状态 Bean(线程不安全)

    有状态 Bean 保存了可变的实例变量,多个线程并发访问时产生竞态条件:

    @Service// ❌ 错误:有状态的单例 BeanpublicclassCounterService{privateintcount=0;// 实例变量,线程共享publicvoidincrement(){count++;// 非原子操作,线程不安全!}publicintgetCount(){returncount;}}

    并发问题演示

    时间线线程 A线程 Bcount 值
    T1读取 count = 00
    T2读取 count = 00
    T3计算 0 + 1 = 10
    T4计算 0 + 1 = 10
    T5写入 count = 11
    T6写入 count = 11

    两个线程各执行一次increment(),预期结果为 2,实际结果为 1,丢失了一次更新

  • 2.3 有状态 Bean 的典型误用场景

    误用场景问题正确做法
    Controller 中保存用户上下文多请求共享状态,数据串乱使用方法参数传递,或 ThreadLocal
    Service 中缓存查询结果到实例变量多线程覆盖缓存使用外部缓存(Redis/Caffeine)
    工具类中保存临时计算状态并发计算结果互相干扰使用局部变量,或改为无状态
    @Autowired的 Bean 被修改依赖对象被替换使用final+ 构造器注入

3. 保证线程安全的五种方案
  • 3.1 方案一:无状态设计(首选)

    将 Bean 设计为无状态,所有数据通过方法参数传递:

    @ServicepublicclassCounterService{// ✅ 无实例变量,天然线程安全publicintincrement(intcount){returncount+1;// 通过参数和返回值传递状态}}

    优势:零同步开销,性能最优,代码最清晰。
    适用场景:Service 层、DAO 层、工具类。

  • 3.2 方案二:不可变对象

    使用final修饰字段,对象创建后不可变:

    @ServicepublicclassConfigService{privatefinalMap<String,String>configMap;// final 引用publicConfigService(@Value("${app.config}")Stringconfig){this.configMap=parseConfig(config);// 构造时初始化,之后不可变}publicStringgetConfig(Stringkey){returnconfigMap.get(key);// 只读操作,线程安全}}

    注意final只保证引用不可变,如果引用对象本身可变(如ArrayList),仍需同步。

  • 3.3 方案三:ThreadLocal(线程隔离)

    ThreadLocal为每个线程提供独立的变量副本,实现线程级隔离:

    @ServicepublicclassRequestContextService{// 每个线程有独立的 SimpleDateFormat 副本privatestaticfinalThreadLocal<SimpleDateFormat>dateFormatHolder=ThreadLocal.withInitial(()->newSimpleDateFormat("yyyy-MM-dd HH:mm:ss"));publicStringformatDate(Datedate){returndateFormatHolder.get().format(date);}}

    ThreadLocal 在 Spring 中的经典应用

    场景使用方式说明
    日期格式化ThreadLocal<SimpleDateFormat>SimpleDateFormat非线程安全
    数据库连接ThreadLocal<Connection>Spring 事务管理器底层实现
    用户上下文ThreadLocal<UserContext>拦截器设置,Service 层获取
    请求追踪ThreadLocal<TraceId>全链路日志追踪

    ⚠️ ThreadLocal 内存泄漏风险

    @ServicepublicclassUserContextHolder{privatestaticfinalThreadLocal<User>currentUser=newThreadLocal<>();publicstaticvoidsetUser(Useruser){currentUser.set(user);}publicstaticUsergetUser(){returncurrentUser.get();}// ✅ 必须在使用完毕后清理!publicstaticvoidclear(){currentUser.remove();// 防止内存泄漏}}// 在拦截器中清理publicclassUserContextInterceptorimplementsHandlerInterceptor{@OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex){UserContextHolder.clear();// 请求结束后清理}}

    内存泄漏原因:ThreadLocal 的键是弱引用(WeakReference<ThreadLocal<?>>),但值是强引用。如果线程池复用线程,线程结束时 ThreadLocal 的键被 GC,但值仍被线程的ThreadLocalMap引用,导致内存泄漏。

    解决方案

    1. 使用完必须remove()
    2. 使用try-finally确保清理
    3. 使用InheritableThreadLocal时注意子线程继承问题
    4. 使用TransmittableThreadLocal(阿里开源)解决线程池传递问题
  • 3.4 方案四:同步机制(synchronized/Lock/Atomic)

    当必须共享可变状态时,使用同步机制:

    @ServicepublicclassCounterService{privatefinalAtomicIntegercount=newAtomicInteger(0);// ✅ 原子操作publicvoidincrement(){count.incrementAndGet();// CAS 无锁,线程安全}publicintgetCount(){returncount.get();}}
    同步方案适用场景性能代码复杂度
    synchronized简单临界区低(阻塞)
    ReentrantLock需要超时/中断/条件变量中(阻塞)
    AtomicInteger/Long简单计数器高(CAS)
    LongAdder高并发计数器极高(分段)
    ConcurrentHashMap并发 Map高(分段锁)
    CopyOnWriteArrayList读多写少列表高(无锁读)
  • 3.5 方案五:改变作用域(prototype/request)

    当 Bean 必须保存状态时,改变作用域避免共享:

    @Component@Scope(value=WebApplicationContext.SCOPE_REQUEST,proxyMode=ScopedProxyMode.TARGET_CLASS)publicclassRequestContext{privateStringtraceId;privateLonguserId;// ... 请求级状态}

    proxyMode的作用

    singletonBean 注入request作用域 Bean 时,由于singletonBean 只创建一次,而requestBean 每个请求都不同,直接注入会导致requestBean 在首次注入后固定不变。

    ScopedProxyMode.TARGET_CLASS(CGLIB 代理)或ScopedProxyMode.INTERFACES(JDK 代理)会为作用域 Bean 创建代理对象,每次调用时从当前作用域(如当前 Request)获取真实实例:

    @Service// singletonpublicclassUserService{@AutowiredprivateRequestContextrequestContext;// 注入的是代理对象publicvoiddoSomething(){// 每次调用都会从当前 Request 获取真实实例StringtraceId=requestContext.getTraceId();}}

4. Spring 各层的线程安全设计规范
层级作用域状态设计线程安全策略
Controllersingleton无状态方法参数传递请求数据,不保存实例变量
Servicesingleton无状态纯业务逻辑,依赖通过注入获取
DAO/Mappersingleton无状态只负责数据访问,不保存查询结果
Entity/POJOprototype有状态每个请求/线程独立实例
配置类singleton不可变final字段,构造时初始化
缓存组件singleton有状态(缓存数据)使用线程安全的缓存(Redis/Caffeine/ConcurrentHashMap)

5. 生产环境避坑指南
  • 5.1 不要在单例 Bean 中使用实例变量保存请求数据

    @RestController// ❌ 致命错误!单例 + 有状态publicclassUserController{privateUsercurrentUser;// 多个请求共享!@GetMapping("/user/{id}")publicUsergetUser(@PathVariableLongid){currentUser=userService.findById(id);// 请求A的数据被请求B覆盖returncurrentUser;}}// ✅ 正确:无状态设计@RestControllerpublicclassUserController{@GetMapping("/user/{id}")publicUsergetUser(@PathVariableLongid){returnuserService.findById(id);// 直接返回,不保存状态}}
  • 5.2 SimpleDateFormat 必须用 ThreadLocal

    SimpleDateFormat是非线程安全的,多线程共享会导致日期解析错误:

    @ServicepublicclassDateService{// ❌ 错误:共享 SimpleDateFormatprivatestaticfinalSimpleDateFormatsdf=newSimpleDateFormat("yyyy-MM-dd");// ✅ 正确:ThreadLocal 隔离privatestaticfinalThreadLocal<SimpleDateFormat>sdfHolder=ThreadLocal.withInitial(()->newSimpleDateFormat("yyyy-MM-dd"));}

    Java 8+ 推荐:使用DateTimeFormatter(线程安全),彻底告别 ThreadLocal:

    privatestaticfinalDateTimeFormatterformatter=DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
  • 5.3 注意 @Async 与 ThreadLocal

    @Async使用线程池执行异步任务,子线程无法继承父线程的ThreadLocal

    @ServicepublicclassUserService{publicvoidprocess(){UserContextHolder.setUser(newUser("admin"));// 主线程设置asyncTask.execute();// 子线程中 UserContextHolder.getUser() = null!}}

    解决方案:使用InheritableThreadLocal或阿里TransmittableThreadLocal(TTL)。

  • 5.4 线程池场景下的 ThreadLocal

    线程池复用线程,如果不清理 ThreadLocal,下一个任务可能读到上一个任务的数据:

    executor.execute(()->{try{ThreadLocalHolder.set(data);// 执行业务逻辑...}finally{ThreadLocalHolder.remove();// ✅ 必须清理}});
  • 5.5 警惕 Spring 代理对象的线程安全

    @Transactional@Cacheable等注解基于 AOP 代理实现,代理对象本身是单例且线程安全的。但目标对象中的实例变量仍然需要开发者保证线程安全。


6. 面试官追问与高分回答模板
  • 追问 1:“Spring 中的 Bean 是线程安全的吗?”

    低分回答:“不是,单例 Bean 多线程共享,有状态时不安全。”(没有区分状态设计)

    高分回答

    "Spring Bean 的线程安全性取决于作用域和状态设计,不能一概而论:

    1. 默认singleton作用域:Spring 容器只创建一个实例,多线程共享。如果 Bean 是无状态的(没有可变实例变量),则天然线程安全;如果 Bean 是有状态的(保存了可变实例变量),则线程不安全
    2. prototype/request/session作用域:每个线程/请求/会话独立实例,天然线程安全,但创建开销大。

    因此,Spring Bean 是否线程安全,核心在于状态设计而非作用域。Spring 官方推荐将 Bean 设计为无状态,这是 Service 层、DAO 层的最佳实践。"

  • 追问 2:“如何保证 Spring Bean 的线程安全?”

    高分回答

    "保证线程安全有五种方案,按推荐优先级排序:

    1. 无状态设计(首选):Bean 不保存实例变量,所有数据通过方法参数传递。零同步开销,性能最优,代码最清晰。Spring 中 99% 的 Bean 应该如此设计。
    2. 不可变对象:使用final字段,对象创建后不可变。注意final只保证引用不可变,引用对象本身可变时仍需同步。
    3. ThreadLocal 线程隔离:为每个线程提供独立变量副本。适用于日期格式化、用户上下文等场景。但必须注意内存泄漏,使用完必须remove()
    4. 同步机制AtomicIntegerConcurrentHashMapsynchronized等。适用于必须共享可变状态的场景。
    5. 改变作用域@Scope("prototype")@Scope("request")配合proxyMode = TARGET_CLASS。适用于必须保存状态且无法重构的场景,但创建开销大。

    推荐优先级:无状态 > 不可变 > ThreadLocal > 同步机制 > 改变作用域。"

  • 追问 3:“ThreadLocal 在 Spring 中怎么用?有什么风险?”

    高分回答

    "ThreadLocal 在 Spring 中的典型应用包括:

    • 日期格式化SimpleDateFormat非线程安全,用 ThreadLocal 隔离;
    • 用户上下文:拦截器设置当前用户,Service 层获取;
    • 数据库连接:Spring 事务管理器底层用 ThreadLocal 绑定连接;
    • 请求追踪:TraceId 全链路传递。

    内存泄漏风险
    ThreadLocal 的键是WeakReference<ThreadLocal<?>>,但值是强引用。线程池场景下,线程复用不结束,ThreadLocalMap 中的值不会被清理,导致内存泄漏。

    解决方案

    1. 使用完必须调用remove()
    2. 使用try-finally确保清理;
    3. 在拦截器的afterCompletion()中清理;
    4. 使用TransmittableThreadLocal(阿里 TTL)解决线程池传递和自动清理问题。

    Java 8+ 替代方案DateTimeFormatter线程安全,可替代ThreadLocal<SimpleDateFormat>。"

  • 追问 4:“@Scope(proxyMode = TARGET_CLASS) 是做什么的?”

    高分回答

    "proxyMode用于解决不同作用域 Bean 的注入问题

    singletonBean(如 Service)注入request作用域 Bean(如 RequestContext)时,Service 只创建一次,如果直接注入 RequestContext,会在首次注入时固定为一个 Request 的实例,后续请求获取的是旧数据。

    ScopedProxyMode.TARGET_CLASS会为requestBean 创建CGLIB 代理对象。Service 注入的是代理对象,每次调用代理对象的方法时,代理会从当前 Request 作用域中获取真实的 Bean 实例,确保每次请求获取的都是当前请求的实例。

    类似地,ScopedProxyMode.INTERFACES使用 JDK 动态代理,要求目标类实现接口。"

  • 追问 5:“Spring 的 @Transactional 是线程安全的吗?”

    高分回答

    "@Transactional本身是线程安全的,原因:

    1. 代理对象线程安全:Spring 为 Bean 创建的 AOP 代理对象是单例的,代理逻辑(开启事务、提交/回滚)是无状态的;
    2. 事务上下文线程隔离:Spring 使用TransactionSynchronizationManager(底层是 ThreadLocal)将数据库连接绑定到当前线程,每个线程有独立的事务上下文;
    3. 事务管理器无状态DataSourceTransactionManager等管理器本身不保存事务状态。

    但需要注意:如果事务方法中修改了 Bean 的实例变量,这些变量仍然是线程共享的,需要开发者自行保证线程安全。@Transactional只保证事务本身的线程安全,不保证业务数据的线程安全。"

  • 追问 6:“你在项目中怎么设计线程安全的 Spring Bean?”

    高分回答

    "我的设计原则是分层的:

    Controller 层:严格无状态,不保存任何实例变量。请求数据通过方法参数(@PathVariable@RequestBody)传递,响应直接返回。

    Service 层:严格无状态,业务逻辑通过参数和返回值传递。需要共享的缓存使用外部服务(Redis),需要计数的使用LongAdder或 Redis。

    DAO/Mapper 层:无状态,只负责数据访问。

    用户上下文:使用 ThreadLocal 传递,在拦截器中设置,在afterCompletion()中清理。Java 8+ 用DateTimeFormatter替代ThreadLocal<SimpleDateFormat>

    配置类:使用final不可变对象,构造器注入。

    唯一使用有状态 Bean 的场景是请求级上下文(如RequestContext),使用@Scope(value = SCOPE_REQUEST, proxyMode = TARGET_CLASS),并确保通过代理访问。"


7. 方案选型速查表
业务场景推荐方案核心理由
Service/DAO 层设计无状态 Bean零同步开销,性能最优,Spring 推荐
配置类不可变对象(final)构造时初始化,之后只读
日期格式化DateTimeFormatter(Java 8+)线程安全,无需 ThreadLocal
用户上下文传递ThreadLocal + 拦截器清理线程隔离,记得 remove()
简单计数器AtomicIntegerCAS 无锁,性能高
高并发计数器LongAdder分段累加,避免 CAS 冲突
请求级状态@Scope(request) + proxyMode请求隔离,通过代理访问
会话级状态@Scope(session) + proxyMode会话隔离
线程池任务上下文TransmittableThreadLocal解决线程池传递和清理问题
并发 MapConcurrentHashMap分段锁,高并发安全
读多写少列表CopyOnWriteArrayList无锁读,写时复制

💡面试官想要的满分总结

Spring Bean 的线程安全性不是框架保证的,而是开发者设计的责任。默认singleton作用域下,无状态 Bean 天然线程安全,有状态 Bean 必须采取保护措施。

理解线程安全必须抓住三个核心:

  1. 状态是根源:线程安全问题的本质是共享可变状态。无状态设计从根本上消除了这个问题,是 Spring 开发的金标准。
  2. ThreadLocal 是双刃剑:它实现了线程隔离,但内存泄漏风险(尤其是线程池场景)必须警惕。使用完必须remove(),Java 8+ 优先用DateTimeFormatter等线程安全类替代。
  3. 作用域代理解决跨域注入singletonBean 注入requestBean 时,必须使用proxyMode = TARGET_CLASS创建作用域代理,确保每次调用获取当前作用域的真实实例。

工程实践中,99% 的 Spring Bean 应该是无状态的。Controller、Service、DAO 层都不应保存实例变量。只有真正的请求级/会话级状态才考虑有状态设计,且必须通过作用域代理或 ThreadLocal 隔离。记住:线程安全不是事后加锁,而是事前设计。


觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询