面試的重點,大廠必問之一:
循環依賴
1. 什麼是循環依賴
看下圖
上圖是循環依賴的三種情況,雖然方式有點不一樣,但是循環依賴的本質是一樣的,就你的完整建立要依賴與我,我的完整建立也依賴于你。互相依賴進而沒法完整建立造成失敗。
2. 代碼示範
我們再通過代碼的方式來示範下循環依賴的效果
public class CircularTest {
public static void main(String[] args) {
new CircularTest1();
}
}
class CircularTest1{
private CircularTest2 circularTest2 = new CircularTest2();
}
class CircularTest2{
private CircularTest1 circularTest1 = new CircularTest1();
}
執行後出現了 StackOverflowError 錯誤
上面的就是最基本的循環依賴的場景,你需要我,我需要你,然後就報錯了。而且上面的這種設計情況我們是沒有辦法解決的。那麼針對這種場景我們應該要怎麼設計呢?這個是關鍵!
3. 分析問題
首先我們要明确一點就是如果這個對象A還沒建立成功,在建立的過程中要依賴另一個對象B,而另一個對象B也是在建立中要依賴對象A,這種肯定是無解的,這時我們就要轉換思路,我們先把A建立出來,但是還沒有完成初始化操作,也就是這是一個半成品的對象,然後在指派的時候先把A暴露出來,然後建立B,讓B建立完成後找到暴露的A完成整體的執行個體化,這時再把B交給A完成A的後續操作,進而揭開了循環依賴的密碼。也就是如下圖:
4. 自己解決
明白了上面的本質後,我們可以自己來嘗試解決下:
先來把上面的案例改為set/get來依賴關聯
public class CircularTest {
public static void main(String[] args) throws Exception{
System.out.println(getBean(CircularTest1.class).getCircularTest2());
System.out.println(getBean(CircularTest2.class).getCircularTest1());
}
private static <T> T getBean(Class<T> beanClass) throws Exception{
// 1.擷取 執行個體對象
Object obj = beanClass.newInstance();
// 2.完成屬性填充
Field[] declaredFields = obj.getClass().getDeclaredFields();
// 周遊處理
for (Field field : declaredFields) {
field.setAccessible(true); // 針對private修飾
// 擷取成員變量 對應的類對象
Class<?> fieldClass = field.getType();
// 擷取對應的 beanName
String fieldBeanName = fieldClass.getSimpleName().toLowerCase();
// 給成員變量指派 如果 singletonObjects 中有半成品就擷取,否則建立對象
field.set(obj,getBean(fieldClass));
}
return (T) obj;
}
}
class CircularTest1{
private CircularTest2 circularTest2;
public CircularTest2 getCircularTest2() {
return circularTest2;
}
public void setCircularTest2(CircularTest2 circularTest2) {
this.circularTest2 = circularTest2;
}
}
class CircularTest2{
private CircularTest1 circularTest1;
public CircularTest1 getCircularTest1() {
return circularTest1;
}
public void setCircularTest1(CircularTest1 circularTest1) {
this.circularTest1 = circularTest1;
}
}
然後我們再通過把對象執行個體化和成員變量指派拆解開來處理。進而解決循環依賴的問題
public class CircularTest {
// 儲存提前暴露的對象,也就是半成品的對象
private final static Map<String,Object> singletonObjects = new ConcurrentHashMap<>();
public static void main(String[] args) throws Exception{
System.out.println(getBean(CircularTest1.class).getCircularTest2());
System.out.println(getBean(CircularTest2.class).getCircularTest1());
}
private static <T> T getBean(Class<T> beanClass) throws Exception{
//1.擷取類對象對應的名稱
String beanName = beanClass.getSimpleName().toLowerCase();
// 2.根據名稱去 singletonObjects 中檢視是否有半成品的對象
if(singletonObjects.containsKey(beanName)){
return (T) singletonObjects.get(beanName);
}
// 3. singletonObjects 沒有半成品的對象,那麼就反射執行個體化對象
Object obj = beanClass.newInstance();
// 還沒有完整的建立完這個對象就把這個對象存儲在了 singletonObjects中
singletonObjects.put(beanName,obj);
// 屬性填充來補全對象
Field[] declaredFields = obj.getClass().getDeclaredFields();
// 周遊處理
for (Field field : declaredFields) {
field.setAccessible(true); // 針對private修飾
// 擷取成員變量 對應的類對象
Class<?> fieldClass = field.getType();
// 擷取對應的 beanName
String fieldBeanName = fieldClass.getSimpleName().toLowerCase();
// 給成員變量指派 如果 singletonObjects 中有半成品就擷取,否則建立對象
field.set(obj,singletonObjects.containsKey(fieldBeanName)?
singletonObjects.get(fieldBeanName):getBean(fieldClass));
}
return (T) obj;
}
}
class CircularTest1{
private CircularTest2 circularTest2;
public CircularTest2 getCircularTest2() {
return circularTest2;
}
public void setCircularTest2(CircularTest2 circularTest2) {
this.circularTest2 = circularTest2;
}
}
class CircularTest2{
private CircularTest1 circularTest1;
public CircularTest1 getCircularTest1() {
return circularTest1;
}
public void setCircularTest1(CircularTest1 circularTest1) {
this.circularTest1 = circularTest1;
}
}
運作程式你會發現問題完美的解決了
在上面的方法中的核心是getBean方法,Test1 建立後填充屬性時依賴Test2,那麼就去建立 Test2,在建立 Test2 開始填充時發現依賴于 Test1,但此時 Test1 這個半成品對象已經存放在緩存到
singletonObjects
中了,是以Test2可以正常建立,在通過遞歸把 Test1 也建立完整了。
最後總結下該案例解決的本質:
5. Spring循環依賴
然後我們再來看看Spring中是如何解決循環依賴問題的呢?剛剛上面的案例中的對象的生命周期的核心就兩個
而Spring建立Bean的生命周期中涉及到的方法就很多了。下面是簡單列舉了對應的方法
基于前面案例的了解,我們知道肯定需要在調用構造方法方法建立完成後再暴露對象,在Spring中提供了三級緩存來處理這個事情,對應的處理節點如下圖:
對應到源碼中具體處理循環依賴的流程如下:
上面就是在Spring的生命周期方法中和循環依賴出現相關的流程了。那麼源碼中的具體處理是怎麼樣的呢?我們繼續往下面看。
首先在調用構造方法的後會放入到三級緩存中
下面就是放入三級緩存的邏輯
protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");
// 使用singletonObjects進行加鎖,保證線程安全
synchronized (this.singletonObjects) {
// 如果單例對象的高速緩存【beam名稱-bean執行個體】沒有beanName的對象
if (!this.singletonObjects.containsKey(beanName)) {
// 将beanName,singletonFactory放到單例工廠的緩存【bean名稱 - ObjectFactory】
this.singletonFactories.put(beanName, singletonFactory);
// 從早期單例對象的高速緩存【bean名稱-bean執行個體】 移除beanName的相關緩存對象
this.earlySingletonObjects.remove(beanName);
// 将beanName添加已注冊的單例集中
this.registeredSingletons.add(beanName);
}
}
}
然後在填充屬性的時候會存入二級緩存中
earlySingletonObjects.put(beanName,bean);
registeredSingletons.add(beanName);
最後把建立的對象儲存在了一級緩存中
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
// 将映射關系添加到單例對象的高速緩存中
this.singletonObjects.put(beanName, singletonObject);
// 移除beanName在單例工廠緩存中的資料
this.singletonFactories.remove(beanName);
// 移除beanName在早期單例對象的高速緩存的資料
this.earlySingletonObjects.remove(beanName);
// 将beanName添加到已注冊的單例集中
this.registeredSingletons.add(beanName);
}
}
6. 疑問點
這些疑問點也是面試官喜歡問的問題點
為什麼需要三級緩存
三級緩存主要處理的是AOP的代理對象,存儲的是一個ObjectFactory
三級緩存考慮的是帶你對象,而二級緩存考慮的是性能-從三級緩存的工廠裡建立出對象,再扔到二級緩存(這樣就不用每次都要從工廠裡拿)
沒有三級環境能解決嗎?
沒有三級緩存是可以解決循環依賴問題的
三級緩存分别什麼作用
一級緩存:正式對象
二級緩存:半成品對象