关注一下 spring 源代码中的一点细节,版本 spring-framework.version 6.2.7
来源 org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// Quick check for existing instance without full singleton lock.
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
if (!this.singletonLock.tryLock()) {
// Avoid early singleton inference outside of original creation thread.
return null;
}
try {
// Consistent creation of early reference within full singleton lock.
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// Singleton could have been added or removed in the meantime.
if (this.singletonFactories.remove(beanName) != null) {
this.earlySingletonObjects.put(beanName, singletonObject);
}
else {
singletonObject = this.singletonObjects.get(beanName);
}
}
}
}
}
finally {
this.singletonLock.unlock();
}
}
}
return singletonObject;
}
我存在两个疑问
- 为什么源代码中从一级、二级缓存中拿不到,获取锁之后又重新尝试从一级、二级缓存中获取呢?
- singletonFactory.getObject() 之后不是直接删除三级缓存 - 更新二级缓存,而是还要从一级缓存获取呢?
singletonObject = singletonFactory.getObject(); // Singleton could have been added or removed in the meantime. if (this.singletonFactories.remove(beanName) != null) { this.earlySingletonObjects.put(beanName, singletonObject); } else { singletonObject = this.singletonObjects.get(beanName); }
Q1
为了应对多线程同时获取同一个 bean 的情况
如果已经存在实例,get 实例时是只读的,此时不加锁直接尝试读取
如果不存在,那意味着可能需要开始加载 bean,会涉及到更改缓存 map
没错,就是下面三位,所谓三级缓存,外加一个相关的可重入锁
来源 org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java
/** Common lock for singleton creation. */
final Lock singletonLock = new ReentrantLock();
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Creation-time registry of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(16);
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
假设有 线程 A 和 线程 B 同时在获取 Bean “X”
- 线程 A 和 线程 B 同时发现在无锁的情况下,一、二级缓存里都没有 “X”。
- 线程 A 抢先拿到了锁(tryLock 成功),进入了内部逻辑,通过三级缓存生成了 “X” 的早期引用,将其放进二级缓存,然后释放了锁。
- 此时,线程 B 拿到了锁。如果此时不重新检查一遍,线程 B 就会再次去三级缓存中生成一次 “X” 的早期引用,这会覆盖线程 A 的工作,导致返回了两个不同的对象,违背了单例(Singleton)的原则。
所以,获取锁之后,必须重新确认在排队等锁的这段时间里,是不是其他线程已经把这个 Bean 创建出来并放到一、二级缓存里了。
Q2
为了处理极端并发或复杂的嵌套循环依赖导致的状态突变,这是主要的逻辑
singletonObject = singletonFactory.getObject(); // 从三级缓存的工厂中获取早期对象
// 尝试从三级缓存中移除这个工厂
if (this.singletonFactories.remove(beanName) != null) {
// 如果移除成功,说明这是我们第一次获取它的早期对象,把它升级到二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
}
else {
// 如果移除失败(返回 null),说明工厂已经被别人移除了!
// 此时从一级缓存里拿完全体 Bean
singletonObject = this.singletonObjects.get(beanName);
}
为什么 this.singletonFactories.remove(beanName) 会返回 null?
当它返回 null 时,意味着三级缓存中已经没有这个 Bean 的工厂了。这怎么可能呢?我们不是加了锁吗?
要搞清楚这个问题,有几个前提
- 三级缓存的意义
- 这里的锁是一个可重入锁
三级缓存本身是为了解决同时存在 AOP 代理 和 循环依赖的情况的
如果只是为了解决循环依赖,两级缓存,或者说两个 map 就足够了
private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(16);
以上是三级缓存的 map,其中值是 ObjectFactory
bean 对应的 ObjectFactory 实际上可以理解为 “提前处理了这个 bean 的生命周期” ,其返回的可能是 bean 的代理实例
在这个 “可能创建代理返回” 的过程中,同样有可能会去实例化其他的 bean,就会开始 “套娃”
由于发生在一个线程 + 可重入锁
这就意味着 singletonFactory.getObject() 过程中有能力、有可能修改 一、二、三级缓存
而 spring 通过防御性编码的方式避免了这种场景下出现 bug
评论区