Spring启动时主线程锁对象致Hazelcast线程阻塞问题求助
问题概述
在Spring 4.1.4及后续版本(最高可升级至4.3.30.RELEASE)中集成Hazelcast 3.11时,应用启动阶段出现死锁:
- 主线程初始化
HolderBean时,通过CachedRepository调用Hazelcast Map的get方法,等待Hazelcast线程返回数据 - Hazelcast线程执行
MapLoader.load方法时,Spring事务切面尝试获取DefaultSingletonBeanRegistry的锁,而该锁已被主线程持有,导致双方阻塞
该问题在Spring 4.1.3.RELEASE中不存在,推测由Spring 4.1.4的内部逻辑变更导致。
相关配置与代码
Spring XML配置
<bean id="holder" class="org.example.api.Holder"> <constructor-arg> <bean factory-bean="cachedRepository" factory-method="getAllValueIds"> <constructor-arg> <util:constant static-field="org.example.api.domain.MyStaticField.SOME_FIELD"/> </constructor-arg> </bean> </constructor-arg> </bean> <bean id="cachedRepository" class="org.example.api.repository.CachedRepository"> <property name="byIdCache"> <bean factory-bean="hazelcastInstance" factory-method="getMap"> <constructor-arg value="byId"/> </bean> </property> </bean>
CachedRepository核心代码
public List<Integer> getAllValueIds(SomeField someField) { return byIdCache.get(someField); }
死锁线程分析
主线程状态(启动线程)
主线程持有DefaultSingletonBeanRegistry的锁(对应对象0x00000006ca38d540),并等待Hazelcast的InvocationFuture返回结果:
"org.example.Launcher.main()" #25 prio=5 os_prio=0 tid=0x000001b6ef442800 nid=0x578 waiting on condition [0x0000000b5f0f9000] java.lang.Thread.State: WAITING (parking) ... at com.hazelcast.map.impl.proxy.MapProxyImpl.get(MapProxyImpl.java:120) at org.example.api.repository.CachedRepository.getAllValueIds(CachedRepository.java:43) at org.example.api.Holder.init(Holder.java:51) ... at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) - locked <0x00000006ca38d540> (a java.util.concurrent.ConcurrentHashMap)
Hazelcast线程状态
Hazelcast的分区操作线程尝试获取上述锁,陷入阻塞,原因是事务切面在查找TransactionManager时触发了BeanFactory的类型检查,而该检查需要获取SingletonBeanRegistry的锁:
"hz.application.partition-operation.thread-4" #89 prio=5 os_prio=0 tid=0x000001b6ed53e000 nid=0x32a4 waiting for monitor entry [0x0000000b637fe000] java.lang.Thread.State: BLOCKED (on object monitor) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:188) - waiting to lock <0x00000006ca38d540> (a java.util.concurrent.ConcurrentHashMap) ... at org.springframework.transaction.interceptor.TransactionAspectSupport.determineQualifiedTransactionManager(TransactionAspectSupport.java:396) ... at org.example.api.cache.MapLoader$$EnhancerBySpringCGLIB$$d59bbb21.load(<generated>) at com.hazelcast.map.impl.MapStoreWrapper.load(MapStoreWrapper.java:165) ...
解决方案(适配Spring 4.1.4~4.3.30)
方案1:延迟Holder Bean的初始化逻辑
避免在Spring上下文初始化阶段调用Hazelcast分布式操作,改为在上下文刷新完成后执行初始化:
- 修改
Holder类,移除构造方法中的初始化逻辑,新增init方法 - 实现
ApplicationListener<ContextRefreshedEvent>,在事件触发时调用Holder.init()
public class Holder implements ApplicationListener<ContextRefreshedEvent> { private List<Integer> valueIds; private CachedRepository cachedRepository; // 注入cachedRepository,而非通过构造参数直接获取数据 public void setCachedRepository(CachedRepository cachedRepository) { this.cachedRepository = cachedRepository; } @Override public void onApplicationEvent(ContextRefreshedEvent event) { this.valueIds = cachedRepository.getAllValueIds(MyStaticField.SOME_FIELD); } }
同时修改XML配置,移除构造参数,改为注入cachedRepository:
<bean id="holder" class="org.example.api.Holder"> <property name="cachedRepository" ref="cachedRepository"/> </bean>
方案2:调整MapLoader的事务传播属性
如果MapLoader.load方法不需要事务,或可以使用非强制事务的传播策略,避免触发TransactionManager的查找逻辑:
在MapLoader的load方法上添加事务注解,指定传播属性为SUPPORTS或NOT_SUPPORTED:
@Transactional(propagation = Propagation.NOT_SUPPORTED) public Object load(Object key) { // 原有加载逻辑 }
这样事务切面不会尝试查找TransactionManager,也就不会触发BeanFactory的锁竞争。
方案3:确保依赖Bean提前初始化
通过depends-on属性,让Holder Bean依赖于MapLoader和TransactionManager相关Bean,确保这些Bean在Holder之前完全初始化:
<bean id="holder" class="org.example.api.Holder" depends-on="mapLoader,transactionManager"> <constructor-arg> <bean factory-bean="cachedRepository" factory-method="getAllValueIds"> <constructor-arg> <util:constant static-field="org.example.api.domain.MyStaticField.SOME_FIELD"/> </constructor-arg> </bean> </constructor-arg> </bean>
注意替换mapLoader和transactionManager为实际的Bean ID。
方案4:懒加载Holder的业务数据
将Holder中的数据获取逻辑改为懒加载,仅在第一次使用时调用Hazelcast:
public class Holder { private volatile List<Integer> valueIds; private CachedRepository cachedRepository; public Holder(CachedRepository cachedRepository) { this.cachedRepository = cachedRepository; } public List<Integer> getValueIds() { if (valueIds == null) { synchronized (this) { if (valueIds == null) { valueIds = cachedRepository.getAllValueIds(MyStaticField.SOME_FIELD); } } } return valueIds; } }
同时修改XML配置,注入cachedRepository而非直接获取数据:
<bean id="holder" class="org.example.api.Holder"> <constructor-arg ref="cachedRepository"/> </bean>
内容的提问来源于stack exchange,提问作者wmel

