Spring 的緩存機制【記錄】

一、背景

在最近的業務需求開發過程中遇到了“傳說中”的循環依賴問題,在之前學習Spring的時候經常會看到Spring是如何解決循環依賴問題的,所謂循環依賴即形成了一個環狀的依賴關系,這個環中的某一個點產生不穩定變化都會導致整個鏈路產生不穩定的變化;此外循環依賴還會導致應用程序啟動失敗、內存溢出、甚至出現一些難以排查的問題,于是便系統性的對該問題進行學習和總結并整理文章如下。

二.、循環依賴

2.1. 什么是循環依賴?

循環依賴指的是多個對象之間的依賴關系形成了一個閉環。圖1、2分別是兩個對象和多個對象形成循環依賴圖示,實際編程中由于依賴層次深、關系復雜等因素,導致依賴關系難以清晰梳理。

圖1:兩個對象間的循環依賴
圖1 兩個對象間的循環依賴
圖2:多個對象間的循環依賴
圖2:多個對象間的循環依賴
2.2. 為什么會產生循環依賴?

Spring創建bean的本質還是創建對象,一個完整的對象包含兩部分:當前對象的實例化和對象屬性的實例化。在Spring中,對象的實例化是通過反射實現的,而對象的屬性則是在對象實例化之后通過一定的方式設置的。

圖3:Spring創建Bean流程
在這里插入圖片描述

接下來以Demo中的類A、B為例描述循環依賴產生過程:

Demo中的類A、B中各自都以對方為自己的全局屬性,并且在Spring中實例化bean是通過ApplicationContext.getBean()方法來進行的。

如果獲取的對象依賴了另一個對象,那么會首先創建當前對象,然后通過遞歸的調用ApplicationContext.getBean()方法來獲取所依賴的對象,最后將獲取到的對象注入到當前對象中。

@Component
public class A {private B b;public void setB(B b) {this.b = b;}
}
@Component
public class B {private A a;public void setA(A a) {this.a = a;}
}

此處以Demo中初始化A對象為例,

第一步:首先Spring嘗試通過ApplicationContext.getBean()方法獲取A對象的實例,由于Spring容器中還沒有A對象實例,因而其會創建一個A對象,然后發現其依賴了B對象,所以會嘗試遞歸的通過ApplicationContext.getBean()方法獲取B對象的實例,但是Spring容器中此時也沒有B對象的實例,因而其還是會先創建一個B對象的實例。( 此時A對象和B對象都已經創建了,并且保存在Spring容器中了,只不過A對象的屬性b和B對象的屬性a都還沒有設置進去。)

第二步:在前面Spring創建B對象之后,Spring發現B對象依賴了屬性a,因而此時還是會嘗試遞歸的調用ApplicationContext.getBean()方法獲取A對象的實例,因為Spring中已經有一個A對象的實例,雖然只是半成品(其屬性b還未初始化),但其也還是目標bean,因而會將該A對象的實例返回。(此時,B對象的屬性a就設置進去了,然后還是ApplicationContext.getBean()方法遞歸的返回,也就是將B對象的實例返回,此時就會將該實例設置到A對象的屬性b中。)

第三步:在上面這個遞歸過程的最后,Spring將獲取到的B對象實例設置到了A對象的屬性b中了,這里的A對象其實和前面設置到實例B中的半成品A對象是同一個對象,其引用地址是同一個,這里為A對象的b屬性設置了值,其實也就是為那個半成品的a屬性設置了值。

實際加載過程流程圖如圖4所示:其中圖中getBean()表示調用Spring的ApplicationContext.getBean()方法,而該方法中的參數,則表示我們要嘗試獲取的目標對象。圖中的黑色箭頭表示一開始的方法調用走向,走到最后,返回了Spring中緩存的A對象之后,表示遞歸調用返回了,此時使用紅色的箭頭表示。從圖中我們可以很清楚的看到,B對象的a屬性是在第三步中注入的半成品A對象,而A對象的b屬性是在第二步中注入的成品B對象,此時半成品的A對象也就變成了成品的A對象,因為其屬性已經設置完成了。

圖4:Bean加載流程圖
在這里插入圖片描述

三、Spring緩存機制

3.1. Spring是如何解決循環依賴的?

Spring在DefaultSingletonBeanRegistry類中維護了三個Map,也就是我們通常說的三級緩存。

singletonObjects (一級緩存) 它是我們最熟悉的朋友,俗稱“單例池”“容器”,緩存創建完成單例Bean的地方。

earlySingletonObjects(二級緩存) 映射Bean的早期引用,也就是說在這個Map里的Bean不是完整的,甚至還不能稱之為“Bean”,只是一個Instance。

singletonFactories(三級緩存) 映射創建Bean的原始工廠。

圖5:Spring三級緩存
在這里插入圖片描述

3.2. Spring源碼之“獲取Bean

接下來結合具體的Spring源碼進行分析,首先分析“獲取Bean”的源碼,注意getSingleton()方法。

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {//第1級緩存 用于存放 已經屬性賦值、完成初始化的 單例Beanprivate final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);//第2級緩存 用于存在已經實例化,還未做代理屬性賦值操作的 單例Beanprivate final Map<String, Object> earlySingletonObjects = new HashMap<>(16);//第3級緩存 存儲創建單例Bean的工廠private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);//已經注冊的單例池里的beanNameprivate final Set<String> registeredSingletons = new LinkedHashSet<>(256);//正在創建中的beanName集合private final Set<String> singletonsCurrentlyInCreation =Collections.newSetFromMap(new ConcurrentHashMap<>(16));//緩存查找bean  如果第1級緩存沒有,那么從第2級緩存獲取。如果第2級緩存也沒有,那么從第3級緩存創建,并放入第2級緩存。protected Object getSingleton(String beanName, boolean allowEarlyReference) {Object singletonObject = this.singletonObjects.get(beanName); //第1級if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {synchronized (this.singletonObjects) {singletonObject = this.earlySingletonObjects.get(beanName); //第2級if (singletonObject == null && allowEarlyReference) {//第3級緩存  在doCreateBean中創建了bean的實例后,封裝ObjectFactory放入緩存的bean實例ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {//創建未賦值的beansingletonObject = singletonFactory.getObject();//放入到第2級緩存this.earlySingletonObjects.put(beanName, singletonObject);//從第3級緩存刪除this.singletonFactories.remove(beanName);}}}}return singletonObject;}   
}

3.3. Spring源碼之“添加到第1級緩存”

其中“添加到第1級緩存”的源碼為:

protected void addSingleton(String beanName, Object singletonObject) {synchronized (this.singletonObjects) {// 放入第1級緩存this.singletonObjects.put(beanName, singletonObject);// 從第3級緩存刪除this.singletonFactories.remove(beanName);// 從第2級緩存刪除this.earlySingletonObjects.remove(beanName);// 放入已注冊的單例池里this.registeredSingletons.add(beanName);}}

添加到“第3級緩存”的源碼為:

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {synchronized (this.singletonObjects) {// 若第1級緩存沒有bean實例if (!this.singletonObjects.containsKey(beanName)) {// 放入第3級緩存this.singletonFactories.put(beanName, singletonFactory);// 從第2級緩存刪除,確保第2級緩存沒有該beanthis.earlySingletonObjects.remove(beanName);// 放入已注冊的單例池里this.registeredSingletons.add(beanName);}}}

“創建Bean”的源碼為下面所示,通過這段代碼,我們可以知道:Spring 在實例化對象之后,就會為其創建一個 Bean 工廠,并將此工廠加入到三級緩存中。

因此,Spring 一開始提前暴露的并不是實例化的 Bean,而是將 Bean 包裝起來的ObjectFactory。為什么要這么做呢?

這實際上涉及到 AOP。如果創建的 Bean 是有代理的,那么注入的就應該是代理 Bean,而不是原始的 Bean。但是,Spring一開始并不知道 Bean是否會有循環依賴,通常情況下(沒有循環依賴的情況下),Spring 都會在“完成填充屬性并且執行完初始化方法”之后再為其創建代理。但是,如果出現了循環依賴,Spring 就不得不為其提前創建"代理對象";否則,注入的就是一個原始對象,而不是代理對象。因此,這里就涉及到"應該在哪里提前創建代理對象。

protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, Object[] args) throws BeanCreationException {BeanWrapper instanceWrapper = null;if (instanceWrapper == null) {//實例化對象instanceWrapper = this.createBeanInstance(beanName, mbd, args);}final Object bean = instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null;Class<?> beanType = instanceWrapper != null ? instanceWrapper.getWrappedClass() : null;//判斷是否允許提前暴露對象,如果允許,則直接添加一個 ObjectFactory 到第3級緩存boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&isSingletonCurrentlyInCreation(beanName));if (earlySingletonExposure) {//添加到第3級緩存addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));}//填充屬性this.populateBean(beanName, mbd, instanceWrapper);//執行初始化方法,并創建代理exposedObject = initializeBean(beanName, exposedObject, mbd);return exposedObject;
}

Spring通過在ObjectFactory中去提前創建代理對象,該對象會執行getObject()方法來獲取Bean。執行方法如下:

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupportimplements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {@Overridepublic Object getEarlyBeanReference(Object bean, String beanName) {Object cacheKey = getCacheKey(bean.getClass(), beanName);// 記錄已被代理的對象this.earlyProxyReferences.put(cacheKey, bean);return wrapIfNecessary(bean, beanName, cacheKey);}
}

提前進行對象的代理工作,并在 earlyProxyReferences map中記錄已被代理的對象,是為了避免在后面重復創建代理對象。

public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupportimplements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {@Overridepublic Object getEarlyBeanReference(Object bean, String beanName) {Object cacheKey = getCacheKey(bean.getClass(), beanName);// 記錄已被代理的對象this.earlyProxyReferences.put(cacheKey, bean);return wrapIfNecessary(bean, beanName, cacheKey);}
}

再次分析獲取bean的方法getSingleton()方法,可知:提前暴露的對象,雖然已實例化,但是沒有進行屬性填充,還沒有完成初始化,是一個不完整的對象。 這個對象存放在二級緩存中,對于三級緩存機制十分重要,是解決循環依賴一個非常巧妙的設計。接下來我們結合Spring緩存機制來分析上面Demo中A、B循環依賴。

A 調用doCreateBean()創建Bean對象:由于還未創建,從第1級緩存singletonObjects查不到,此時只是一個半成品(提前暴露的對象),放入第3級緩存singletonFactories。

A在屬性填充時發現自己需要B對象,但是在三級緩存中均未發現B,于是創建B的半成品,放入第3級緩存singletonFactories。

B在屬性填充時發現自己需要A對象,從第1級緩存singletonObjects和第2級緩存earlySingletonObjects中未發現A,但是在第3級緩存singletonFactories中發現A,將A放入第2級緩存earlySingletonObjects,同時從第3級緩存singletonFactories刪除。

將A注入到對象B中。

B完成屬性填充,執行初始化方法,將自己放入第1級緩存singletonObjects中(此時B是一個完整的對象),同時從第3級緩存singletonFactories和第2級緩存earlySingletonObjects中刪除。

A得到“對象B的完整實例”,將B注入到A中。

A完成屬性填充,執行初始化方法,并放入到第1級緩存singletonObjects中。

在創建過程中,都是從第三級緩存(對象工廠創建不完整對象),將提前暴露的對象放入到第二級緩存;從第二級緩存拿到后,完成初始化,并放入第一級緩存。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/215545.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/215545.shtml
英文地址,請注明出處:http://en.pswp.cn/news/215545.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

OpenCV-opencv下載安裝和基本操作

文章目錄 一、實驗目的二、實驗內容三、實驗過程OpenCV-python的安裝與配置python下載和環境配置PIP鏡像安裝Numpy安裝openCV-python檢驗opencv安裝是否成功 openCV-python的基本操作圖像輸入和展示以及寫出openCV界面編程單窗口顯示多圖片鼠標事件鍵盤事件滑動條事件 四、實驗…

唯創知音WTN6080-8S語音芯片在咖啡機中的應用:增添聲音魅力,提升用戶體驗

在快節奏的現代生活中&#xff0c;咖啡機已成為許多家庭和辦公室的必備設備&#xff0c;為人們提供了便捷和高品質的咖啡享受。然而&#xff0c;對于很多用戶來說&#xff0c;操作咖啡機可能是一項復雜而棘手的任務。為了解決這一難題&#xff0c;唯創知音WTN6080-8S語音芯片被…

Altman作了多少惡?排擠首席科學家出GPT5開發、離間董事會、PUA員工

在山姆奧特曼&#xff08;Sam Altman&#xff09;被OpenAI董事會突然解職后的幾天里&#xff0c;這個消息在科技圈引發轟動&#xff0c;該公司內部員工和許多科技界人士甚至將此舉比作一場政變。 奧特曼被解雇后立即傳出的說法是&#xff0c;OpenAI的廣大員工都很喜歡他&#x…

一入一出模擬量兩線制無源 4-20mA隔離變送器

一入一出模擬量兩線制無源 4-20mA隔離變送器 特征與應用&#xff1a; ◆薄體積&#xff0c;低成本&#xff0c;國際標準 DIN35mm 導軌安裝方式 ◆兩端隔離(輸入、輸出間相互隔離) ◆單通道輸入單通道輸出 ◆高精度等級(0.1%,0.2% F.S) ◆高線性度(0.1% F.S) ◆高隔離電壓(3000…

32位ADC布局的指導方針

接地必須是一個低阻抗連接&#xff0c;以使回流電流不受干擾地流回各自的源。接地面連接盡量短且直。使用過孔連接接地線時&#xff0c;應并聯多個過孔&#xff0c;以減小對地阻抗。 混合信號布局有時包含在一個位置捆綁在一起的單獨的模擬和數字地平面;但是&#xff0c;當模擬…

活動回顧 | 菊風亮相 GTC2023 全球流量大會

2023年12月5日-12月6日&#xff0c;由白鯨出海主辦的【GTC2023 全球流量大會】在中國深圳盛大召開。 本次大會薈聚海內外優質企業品牌&#xff0c;以專業的“展會”形式&#xff0c;全方位呈現跨境出海的成果&#xff0c;探索多元化的跨境商業模式&#xff0c;大會涵蓋社交娛樂…

【Jeecg Boot 3 - 第二天】1.1、后端 docker-compose 部署 JEECGBOOT3

一、場景 二、實戰 ? 2.1 修改配置文件 &#xff1e; 目的一&#xff1a;將 dev 變更為生產環境 prod &#xff1e; 目的二&#xff1a;方便spring項目調用docker同個network下的redis和mysql ? 2.2 編寫dockerfile ? 2.3 編寫docker-compose.yaml ? 2.4 打…

Qt/C++音視頻開發59-使用mdk-sdk組件/原qtav作者力作/性能兇殘/超級跨平臺

一、前言 最近一個月一直在研究mdk-sdk音視頻組件&#xff0c;這個組件是原qtav作者的最新力作&#xff0c;提供了各種各樣的示例demo&#xff0c;不僅限于支持C&#xff0c;其他各種比如java/flutter/web/android等全部支持&#xff0c;性能上也是杠杠的&#xff0c;目前大概…

cadence中如何在更新原理圖封裝

cadence中如何在更新原理圖封裝 一、更改原理圖封裝 當原理圖畫好后&#xff0c;如果我們發現某個封裝錯了&#xff0c;需要改動&#xff0c;需要找到你最初畫Library中器件封裝文件打開&#xff0c;進行修改&#xff0c;修改好后保存。 二、更新封裝 保存好后&#xff0c;…

C/C++ 有效的字母異位詞

題目&#xff1a; 給定兩個字符串s和t&#xff0c;編寫一個函數來判斷t是否是s的字母異位詞。 注&#xff1a;若s和t中每個字符出現的次數都相同&#xff0c;則稱s和t互為字母異位詞。 示例 1: 輸入: s "anagram", t "nagaram" 輸出: true …

2024年廣西職業院校技能大賽中職組《網絡安全》賽項樣題

2024年廣西職業院校技能大賽 中職組《網絡安全》賽項樣題 目錄 任務一 登錄安全加固 任務二 數據庫加固&#xff08;Data&#xff09; 任務三 Web安全加固(Web) 任務四 流量完整性保護&#xff08;Web,Data&#xff09; 任務五 事件監控 任務一 應急響應 任務二 …

295. 數據流的中位數

二分法實現 295. 數據流的中位數 295. 數據流的中位數 本題的第一個難點&#xff0c;要自己構造一個類&#xff08;因為個人構造類的題目做的較少&#xff09; 屬性&#xff1a; 數組的長度int 數組的數據結構 List保證原數組是一個有序數組&#xff0c;我使用了二分查找插入新…

【IDEA】反向撤銷操作快捷鍵 ctrl+shift+z 和搜狗熱鍵沖突的解決辦法

當我們執行某些操作時與搜狗熱鍵沖突&#xff0c;直接取消搜狗的快捷鍵即可&#xff01;&#xff01;&#xff01;以下以 ctrlshiftz 為例。 在輸入懸浮框右鍵找到更多設置 按鍵里面找到系統功能快捷鍵設置 取消掉沖突的熱鍵即可

?gzip --- 對 gzip 格式的支持?

源代碼&#xff1a; Lib/gzip.py 此模塊提供的簡單接口幫助用戶壓縮和解壓縮文件&#xff0c;功能類似于 GNU 應用程序 gzip 和 gunzip。 數據壓縮由 zlib 模塊提供。 gzip 模塊提供 GzipFile 類和 open()、compress()、decompress() 幾個便利的函數。GzipFile 類可以讀寫 gz…

Codeforces Round 911 (Div. 2)

Codeforces Round 911 (Div. 2) A 有大于3的區間就可以無限取水&#xff0c;答案為2&#xff0c;其他的按照個數 #include <bits/stdc.h>using namespace std;void solve() {int n, k 0;cin >> n;string s, t "...";cin >> s;auto it search…

ARM day6

2.串口發送指令控制硬件工作 結果&#xff1a; uart.h #ifndef __UART_H__ #define __UART_H__ #include "stm32mp1xx_gpio.h" #include "stm32mp1xx_rcc.h" #include "stm32mp1xx_uart.h"void myuart4_init(); void myputchar(char i); cha…

element-ui基本使用

基本使用&#xff1a;npm i element-uimain.js&#xff1a;/*** 該文件是整個項目的入口文件*/ import Vue from vue; import App from ./App.vue; import ElementUI from element-ui; // 引入Element全部樣式 import element-ui/lib/theme-chalk/index.css;// 關閉vue的生產提…

C++ this指針

通常情況下&#xff0c;類的成員函數都只涉及一個對象&#xff0c;即調用它的對象。但有時候方法可能涉及到兩個對象&#xff0c;在這種情況就需要使用到C的this指針。 class Stock { private: ... double total_val; ... public: double total() const {return total_val;} }…

【Linux】進程周邊002之進程狀態

&#x1f440;樊梓慕&#xff1a;個人主頁 &#x1f3a5;個人專欄&#xff1a;《C語言》《數據結構》《藍橋杯試題》《LeetCode刷題筆記》《實訓項目》《C》《Linux》 &#x1f31d;每一個不曾起舞的日子&#xff0c;都是對生命的辜負 目錄 前言 1.什么是狀態&#xff1f; …

ChatGLM 手記

ChatGLM-6B&#xff1a;GitHub - THUDM/ChatGLM-6B: ChatGLM-6B: An Open Bilingual Dialogue Language Model | 開源雙語對話語言模型 ChatGLM3&#xff1a; GitHub - THUDM/ChatGLM3: ChatGLM3 series: Open Bilingual Chat LLMs | 開源雙語對話語言模型 chatglm.cpp&…