侧边栏壁纸
  • 累计撰写 12 篇文章
  • 累计创建 5 个标签
  • 累计收到 2 条评论
标签搜索

目 录CONTENT

文章目录

spring 源代码中的 三级缓存和双重检查锁

关注一下 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;
	}

我存在两个疑问

  1. 为什么源代码中从一级、二级缓存中拿不到,获取锁之后又重新尝试从一级、二级缓存中获取呢?
  2. 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”

  1. 线程 A 和 线程 B 同时发现在无锁的情况下,一、二级缓存里都没有 “X”。
  2. 线程 A 抢先拿到了锁(tryLock 成功),进入了内部逻辑,通过三级缓存生成了 “X” 的早期引用,将其放进二级缓存,然后释放了锁。
  3. 此时,线程 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 的工厂了。这怎么可能呢?我们不是加了锁吗?

要搞清楚这个问题,有几个前提

  1. 三级缓存的意义
  2. 这里的锁是一个可重入锁

三级缓存本身是为了解决同时存在 AOP 代理 和 循环依赖的情况的
如果只是为了解决循环依赖,两级缓存,或者说两个 map 就足够了

private final Map<String, ObjectFactory<?>> singletonFactories = new ConcurrentHashMap<>(16);

以上是三级缓存的 map,其中值是 ObjectFactory
bean 对应的 ObjectFactory 实际上可以理解为 “提前处理了这个 bean 的生命周期” ,其返回的可能是 bean 的代理实例
在这个 “可能创建代理返回” 的过程中,同样有可能会去实例化其他的 bean,就会开始 “套娃”
由于发生在一个线程 + 可重入锁
这就意味着 singletonFactory.getObject() 过程中有能力、有可能修改 一、二、三级缓存
而 spring 通过防御性编码的方式避免了这种场景下出现 bug

评论区