@Stateless注入@Singleton/@ApplicationScoped的线程安全性及相关疑问
咱们先逐个拆解你的疑问,从基础场景到特殊情况,再到作用域选型的本质差异:
一、你的论断对版本1/2/3的适用性及差异分析
先明确你提出的核心论断:
若将@Stateless Bean注入任意类型的@Singleton或@ApplicationScoped Bean,每次调用persistUser()时,容器池都会提供另一个(不一定是同一个)MyStateless实例。因此@Stateless Bean与@Singleton/@ApplicationScoped Bean之间不存在1:1关联。这也意味着,@Stateless Bean中注入的容器管理EntityManager,通过persistUser()被@Singleton/@ApplicationScoped Bean间接使用时是线程安全的。
这个论断完全适用于三个版本,不过三个版本在单例Bean自身的并发控制上有差异,但对Stateless Bean的处理逻辑是一致的:
版本1:@javax.ejb.Singleton(默认容器级并发)
EJB Singleton默认用ConcurrencyManagementType.CONTAINER,容器会保证同一时间只有一个线程进入Singleton的业务方法(除非你用@Lock(LockType.READ)放宽限制)。但注意:Singleton持有的MyStateless是代理对象,不是真实实例。每次调用stateless.persist()时,代理都会从Stateless池中获取一个可用实例执行——所以不存在1:1绑定,EntityManager的线程安全也能保证。
版本2:@ApplicationScoped(CDI应用作用域)
CDI的ApplicationScoped是单例,但它注入EJB Stateless Bean时,同样会用代理包装。CDI把EJB Stateless视为“伪作用域”,每次调用代理方法都会委托给容器池中的Stateless实例。和版本1的区别是,ApplicationScoped Bean本身没有容器级的并发控制(需要自己加同步逻辑),但Stateless的代理逻辑和EJB场景完全一致——论断依然成立。
版本3:@javax.ejb.Singleton + @ConcurrencyManagement(BEAN)
这个版本把并发管理交给Bean自己,多个线程可以同时进入persistUser()。但关键是:Singleton持有的还是Stateless的代理,每个线程调用stateless.persist()时,代理都会分配独立的Stateless实例(可能复用空闲的,但每个实例同一时间只会被一个线程使用)。所以论断还是适用:没有1:1关联,EntityManager线程安全。
三个版本的核心差异在单例Bean自身的并发控制策略,但对Stateless Bean的容器调度逻辑没有区别——都是代理+池化实例的模式。
二、版本4:多线程定时调用的线程安全性
版本4的代码是Singleton用ManagedScheduledExecutorService启动5个定时任务,同时调用Stateless Bean的persist()方法。你的判断是对的:完全线程安全。
原因很简单:
- 虽然多个定时线程同时调用
stateless.persist(),但Stateless的代理会为每个线程请求分配独立的Stateless实例(从容器池里取)。 - 每个Stateless实例中的EntityManager是事务作用域的(TransactionScoped),它的代理会为每个线程/事务绑定独立的底层Hibernate Session(就是你日志里的
emDelegate)。 - 哪怕多个线程用同一个EntityManager代理,代理内部会通过线程上下文区分请求,路由到不同的底层Session,所以不会有线程安全问题。
三、EntityManager代理的线程安全逻辑
你观察到的日志现象非常关键:同一个emProxy但不同的emDelegate,这个正是容器保证线程安全的核心设计:
- EntityManager代理本身是线程安全的单例(或者说同一个Bean实例共享一个代理),它内部会维护线程上下文,把每个线程的请求转发到对应的底层持久化上下文(Session)。
- 根本不需要每个线程用不同的代理对象,代理的存在就是为了让多个线程安全复用,同时保证每个线程拿到独立的底层Session。线程安全的核心是底层Session的独立性,而不是代理对象的唯一性。
四、为什么选@Stateless而非@ApplicationScoped?
你提到把MyStateless改成@ApplicationScoped后好像也能工作,但两者有本质差异,选Stateless的理由包括:
- 实例池化与高并发支持:Stateless Bean由容器动态池化,根据负载调整实例数量,高并发下性能更好;ApplicationScoped是单实例,所有请求共享同一个Bean,如果后续在Bean里加了非线程安全的成员变量,立刻会出并发问题——而Stateless因为每个线程用不同实例,天然避免这个问题。
- 事务管理便利性:EJB Stateless默认支持容器管理事务(CMT),可以用
@TransactionAttribute灵活控制事务行为;CDI ApplicationScoped Bean需要手动配置事务(比如@Transactional,不同实现细节有差异)。 - EJB专属特性:Stateless可以用EJB的远程调用、异步方法、定时器等特性,而ApplicationScoped是CDI的基础作用域,没有这些能力。
- 生命周期与资源回收:Stateless实例在空闲时会被容器销毁回收,节省资源;ApplicationScoped Bean从应用启动到关闭一直存在,长期占用资源。
五、对最终结论的验证
你的结论:“只要代理的委托对象不同,同一代理即可用于并发运行的线程。代理类型(EntityManager、无状态Bean等)无关紧要。”——这个结论是正确的,但要补充一个前提:代理本身必须是线程安全的设计(Java EE中的EntityManager代理、Stateless Bean代理都是符合这个要求的)。代理的核心职责就是把多线程请求路由到独立的委托实例,或者保证委托实例同一时间只被一个线程使用,这也是容器提供这些代理的初衷。
内容的提问来源于stack exchange,提问作者nimo23

