Java內(nèi)存模型JMM與volatile
1.Java內(nèi)存模型
JAVA定義了一套在多線程讀寫共享數(shù)據(jù)時(shí)時(shí),對(duì)數(shù)據(jù)的可見性、有序性和原子性的規(guī)則和保障。屏蔽掉不同操作系統(tǒng)間的微小差異。
Java內(nèi)存模型(Java Memory Model)是一種抽象的概念,并不真實(shí)存在,它描述的是一組規(guī)則或規(guī)范(定義了程序中各個(gè)變量的訪問(wèn)方式)。 JVM運(yùn)行程序的實(shí)體是線程,而每個(gè)線程創(chuàng)建時(shí) JVM 都會(huì)為其創(chuàng)建一個(gè)工作內(nèi)存(??臻g),用于存儲(chǔ)線程私有的數(shù)據(jù),而Java 內(nèi)存模型中規(guī)定所有變量都存儲(chǔ)在主內(nèi)存,主內(nèi)存是共享內(nèi)存區(qū)域,所有線程都可以訪問(wèn), 但線程對(duì)變量的操作(讀取賦值等)必須在工作內(nèi)存中進(jìn)行。所以首先要將變量從主內(nèi)存拷貝的自己的工作內(nèi)存空間,然后對(duì)變量進(jìn)行操作,操作完成后再將變量寫回主內(nèi)存,不能直接操作主內(nèi)存中的變量。工作內(nèi)存是每個(gè)線程的私有數(shù)據(jù)區(qū)域,因此不同的線程間無(wú)法訪問(wèn)對(duì)方的工作內(nèi)存,線程間的通信(傳值)必須通過(guò)主內(nèi)存來(lái)完成。
基于JMM規(guī)范的線程,工作內(nèi)存,主內(nèi)存工作交互圖 :
- 主內(nèi)存: 線程的共享數(shù)據(jù)區(qū)域,主要存儲(chǔ)的是Java實(shí)例對(duì)象,所有線程創(chuàng)建的實(shí)例對(duì)象都存放在主內(nèi)存中(包括局部變量、類信息、常量、靜態(tài)變量)。
- 工作內(nèi)存: 線程私有,主要存儲(chǔ)當(dāng)前方法的所有本地變量信息(主內(nèi)存中的變量副本拷貝) , 每個(gè)線程只能訪問(wèn)自己的工作內(nèi)存,即線程中的本地變量對(duì)其它線程是不可見的,即使訪問(wèn)的是同一個(gè)共享變量。
對(duì)于一個(gè)實(shí)例對(duì)象中的成員方法: 如果方法中包含本地變量是基本數(shù)據(jù)類型,將直接存儲(chǔ)在工作內(nèi)存的幀棧結(jié)構(gòu)中,如果是引用類型,那么該變量的引用會(huì)存儲(chǔ)在功能內(nèi)存的幀棧中,而對(duì)象實(shí)例將存儲(chǔ)在主內(nèi)存(共享數(shù)據(jù)區(qū)域,堆)中。
需要注意的是,在主內(nèi)存中的實(shí)例對(duì)象可以被多線程共享,倘若兩個(gè)線程同時(shí)調(diào)用了同一個(gè)對(duì)象的同一個(gè)方法,那么兩條線程會(huì)將要操作的數(shù)據(jù)拷貝一份到自己的工作內(nèi)存中,執(zhí)行完成操作后才刷新到主內(nèi)存
下面是線程讀取共享變量count執(zhí)行count + 1 操作的過(guò)程:
數(shù)據(jù)同步八大原子操作:
- (1)lock(鎖定): 作用于主內(nèi)存的變量,把一個(gè)變量標(biāo)記為一條線程獨(dú)占狀態(tài)
- (2)unlock(解鎖): 作用于主內(nèi)存的變量,把一個(gè)處于鎖定狀態(tài)的變量釋放出來(lái),釋放后 的變量才可以被其他線程鎖定
- (3)read(讀取): 作用于主內(nèi)存的變量,把一個(gè)變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存 中,以便隨后的load動(dòng)作使用
- (4)load(載入): 作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工 作內(nèi)存的變量副本中
- (5)use(使用): 作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量值傳遞給執(zhí)行引擎
- (6)assign(賦值): 作用于工作內(nèi)存的變量,它把一個(gè)從執(zhí)行引擎接收到的值賦給工作內(nèi) 存的變量
- (7)store(存儲(chǔ)): 作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存 中,以便隨后的write的操作
- (8)write(寫入): 作用于工作內(nèi)存的變量,它把store操作從工作內(nèi)存中的一個(gè)變量的值 傳送到主內(nèi)存的變量中
2.并發(fā)三大特性
2.1.原子性
定義: 一個(gè)操作在CPU中不可以中途暫停再調(diào)度,要么全部執(zhí)行完成,要么全部都不執(zhí)行
問(wèn)題: 兩個(gè)線程對(duì)初始值的靜態(tài)變量一個(gè)做自增,一個(gè)做自減同樣做10000次的結(jié)果很可能不是 0
解決關(guān)鍵字: synchronized、ReentrantLock 建議:
- 用sychronized對(duì)對(duì)象加鎖的力度建議大一點(diǎn)(減少加解鎖次數(shù))
- 鎖住同一個(gè)對(duì)象
2.2.可見性
定義: 當(dāng)多個(gè)線程訪問(wèn)同一個(gè)變量時(shí),一個(gè)線程修改了這個(gè)變量的值,其他線程立即看得到修改的值。(即時(shí)性)
問(wèn)題: 兩個(gè)線程在不同的 CPU ,若線程1改變了變量 i 的值,還未刷新到主存,線程2又使用了 i,那么線程2看到的這個(gè)值肯定還是之前的
//線程1 boolean stop = false; while(stop){ .... } //線程2 stop = true; //并未退出循環(huán)
解決關(guān)鍵字: synchronized、volatile
volatile 關(guān)鍵字,它可以用來(lái)修飾成員變量和靜態(tài)成員變量,避免線程從自己的工作內(nèi)存中查找變量值,必須到主存中獲取它的值,線程操作volatile變量都是直接操作主內(nèi)存,還可以禁止指令重排。 synchronized語(yǔ)句塊既可以保證代碼的原子性,也可以保證代碼塊內(nèi)部的可見性,但是呢synchronized屬于重量級(jí)操作,性能相對(duì)更低
注意: 對(duì)于上述循環(huán)代碼塊,加入System.out.println(); 會(huì)退出循環(huán),因?yàn)?println 被 synchronized 修飾,所有,不要隨便在代碼中使用這種打印語(yǔ)句,會(huì)極度影響程序性能。
2.3.有序性
定義: 虛擬機(jī)在進(jìn)行代碼編譯時(shí),對(duì)改變順序后不會(huì)對(duì)最終結(jié)果造成影響的代碼,虛擬機(jī)不一定會(huì)按我們寫的代碼順序運(yùn)行,有可能進(jìn)行重排序。實(shí)際上雖然重排后不會(huì)對(duì)變量值有影響,但會(huì)造成線程安全問(wèn)題。
解決關(guān)鍵字: synchronized、ReentrantLock volatile關(guān)鍵字,可以禁止指令重排
指令重排: JIT 編譯器在運(yùn)行時(shí)的一些優(yōu)化,可以提升 CPU 的執(zhí)行效率,不讓 CPU 空閑下來(lái)。對(duì)改變順序后不會(huì)對(duì)最終結(jié)果造成影響的代碼,虛擬機(jī)不一定會(huì)按我們寫的代碼順序運(yùn)行,有可能進(jìn)行重排序。比如說(shuō),我兩行代碼 X 和 Y,虛擬機(jī)認(rèn)為它們倆的執(zhí)行順序不影響程序結(jié)果,但 Y 已經(jīng)在 CacheLine 中存在了,就會(huì)優(yōu)先執(zhí)行 Y。
分析下面?zhèn)未a的運(yùn)行情況(r.r1的值):
int num = 0; boolean ready = false; // 線程1 執(zhí)行此方法 public void action1(I_Result r) { if(ready) { r.r1 = num + num; } else { r.r1 = 1; } } // 線程2 執(zhí)行此方法 public void action2(I_Result r) { num = 2; ready = true; } 情況1:線程1 先執(zhí)行,此時(shí) ready = false,所有進(jìn)入else ,結(jié)果為1 情況2:線程2 先執(zhí)行 num = 2,但還沒(méi)來(lái)得及執(zhí)行 ready = true,線程1 開始執(zhí)行,還是進(jìn)入else ,結(jié)果為1 情況3:線程2 先執(zhí)行到ready = true,線程1 執(zhí)行,進(jìn)入else ,結(jié)果為4 情況4:指令重排導(dǎo)致,線程2執(zhí)行 ready = true,切換到線程1,進(jìn)入 if 分支,相加為0,再切回線程2,執(zhí)行 num = 2,結(jié)果為0
double-checked locking 單例模式: 也存在指令重排問(wèn)題(不使用volatile,對(duì)象實(shí)例化是原子操作,但分為幾步,每一步又不是原子操作),因此需要在對(duì)象前加上 volatile 關(guān)鍵字防止指令重排,這也是個(gè)非常經(jīng)典的禁止指令重排的例子。
public class SingleLazy { ? ?private SingleLazy() {} ? ?private volatile static SingleLazy INSTANCE; // 獲取實(shí)體 ? ?public static SingleLazy getInstance() { ? ? ? ?// 實(shí)例未被創(chuàng)建,開啟同步代碼塊準(zhǔn)備創(chuàng)建 ? ? ? ?if (INSTANCE == null) { ? ? ? ? ? ?synchronized (SingleLazy.class) { ? ? ? ? ? ? ? ?// 也許其他線程在判斷完后已經(jīng)創(chuàng)建,再次判斷 ? ? ? ? ? ? ? ?if (INSTANCE == null) { ? ? ? ? ? ? ? ? ? ?INSTANCE = new SingleLazy(); ? ? ? ? ? ? ? } ? ? ? ? ? } ? ? ? } ? ? ? ?return INSTANCE; ? } }
創(chuàng)建對(duì)象可以大致分為三步,其中第一步和第二步可能會(huì)發(fā)生指令重排導(dǎo)致安全性問(wèn)題:
memory = allocate();//1.分配對(duì)象內(nèi)存空間 instance(memory);//2.初始化對(duì)象 instance = memory;//3.設(shè)置instance指向剛分配的內(nèi)存地址,此時(shí)instance e != null
注意: JDK1.5前的 volatile 關(guān)鍵字不保證指令重排問(wèn)題
3.兩個(gè)規(guī)則
as-if-serial 語(yǔ)義保證單線程內(nèi)程序的執(zhí)行結(jié)果不被改變,happens-before關(guān)系保證正確同步的多線程程序的執(zhí)行結(jié)構(gòu)不被改變
3.1.happens-before規(guī)則
定義: 如果一個(gè)操作 happens-before 另一個(gè)操作,那么第一個(gè)操作的執(zhí)行結(jié)果將對(duì)第二個(gè)操作可見,而且第一個(gè)操作的執(zhí)行順序排在第二個(gè)操作之前。
兩個(gè)操作之間存在 happens-before 關(guān)系,并不意味 java 平臺(tái)的具體實(shí)現(xiàn)必須按照 happens-before 關(guān)系指定的順序來(lái)執(zhí)行。如果重排序后的執(zhí)行結(jié)構(gòu),與按 happens-before 關(guān)系來(lái)執(zhí)行的結(jié)果一致,那么這種重排序并不非法(JMM允許這種重排序),happens-before 原則內(nèi)容如下:
程序順序原則 即在一個(gè)線程內(nèi)必須保證語(yǔ)義串行性,也就是說(shuō)按照代碼順序執(zhí)行,(時(shí)間上)先執(zhí)行的操作happen-before(時(shí)間上后執(zhí)行的操作)
鎖規(guī)則 解鎖(unlock)操作必然發(fā)生在后續(xù)的同一個(gè)鎖的加鎖(lock)之前,也就是說(shuō),如果對(duì)于一個(gè)鎖解鎖后,再加鎖,那么加鎖的動(dòng)作必須在解鎖動(dòng)作之后(同一個(gè)鎖)。
volatile規(guī)則 volatile變量的寫,先發(fā)生于讀,這保證了volatile變量的可見性,簡(jiǎn) 單的理解就是,volatile變量在每次被線程訪問(wèn)時(shí),都強(qiáng)迫從主內(nèi)存中讀該變量的 值,而當(dāng)該變量發(fā)生變化時(shí),又會(huì)強(qiáng)迫將最新的值刷新到主內(nèi)存,任何時(shí)刻,不同的線程總是能夠看到該變量的最新值。
線程啟動(dòng)規(guī)則 線程的start()方法先于它的每一個(gè)動(dòng)作,即如果線程A在執(zhí)行線程B 的start方法之前修改了共享變量的值,那么當(dāng)線程B執(zhí)行start方法時(shí),線程A對(duì)共享 變量的修改對(duì)線程B可見
傳遞性 A先于B ,B先于C 那么A必然先于C
線程終止規(guī)則 線程的所有操作先于線程的終結(jié),Thread.join()方法的作用是等待 當(dāng)前執(zhí)行的線程終止。假設(shè)在線程B終止之前,修改了共享變量,線程A從線程B的 join方法成功返回后,線程B對(duì)共享變量的修改將對(duì)線程A可見。
線程中斷規(guī)則 對(duì)線程 interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到 中斷事件的發(fā)生,可以通過(guò)Thread.interrupted()方法檢測(cè)線程是否中斷。
對(duì)象終結(jié)規(guī)則 對(duì)象的構(gòu)造函數(shù)執(zhí)行,結(jié)束先于finalize()方法
3.2.as-if-serial
不管怎么重排序(編譯器和處理器為了提高并行度),(單線程)程序的執(zhí)行結(jié)構(gòu)不能被改變。為了遵守as-if-serial 語(yǔ)義,編譯器和處理器不會(huì)存在數(shù)據(jù)依賴關(guān)系的操作做重排序,但是如果操作之前不存在數(shù)據(jù)依賴關(guān)系,這些操作就可能被編譯器和處理器重排序。
4.volatile
volatile是Java虛擬機(jī)提供的輕量級(jí)的同步機(jī)制,可以保證可見性,但無(wú)法保證原子性。 作用:
- 保證可見性,也就是當(dāng)一個(gè)線程修改了一個(gè)被volatile修飾共享變量的值,新值總是可以被其他線程立即得知。即時(shí)可見通過(guò)緩存一致性協(xié)議保證。
- 禁止指令重排優(yōu)化。通過(guò)內(nèi)存屏障實(shí)現(xiàn)。
//示例 //并發(fā)場(chǎng)景下,count++操作不具備原子性,分為兩步先讀取值,再寫回,會(huì)出現(xiàn)線程安全問(wèn)題 public class VolatileVisibility { public static volatile int count = 0; public static void increase(){ count++; } }
4.1.volatile 禁止重排優(yōu)化的實(shí)現(xiàn)
volatile 變量通過(guò)內(nèi)存屏障實(shí)現(xiàn)其可見性和禁止重排優(yōu)化。
內(nèi)存屏障: 又稱內(nèi)存柵欄,是一個(gè)CPU指令,它的作用有兩個(gè),一是保證特定操作的執(zhí)行順序,二是保證某些變量的內(nèi)存可見性。編譯器和處理器都能執(zhí)行指令重排優(yōu)化。Intel 硬件提供了一系列的內(nèi)存屏障,主要有:Ifence(讀屏障)、sfence(寫屏障)、mfence(全能屏障,包括讀寫)、Lock前綴等。不同的硬件實(shí)現(xiàn)內(nèi)存屏障的方式不同,Java 內(nèi)存模型屏蔽了這種底層硬件平臺(tái)的差異,由 JVM 來(lái)為不同的平臺(tái)生成相應(yīng)的機(jī)器碼。 JVM 中提供了四類內(nèi)存屏障指令:
屏障類型 | 指令 | 說(shuō)明 |
---|---|---|
LoadLoad | Load1; LoadLoad; Load2 | 保證load1的讀取操作在load2及后續(xù)讀取操作之前執(zhí)行 |
StoreStore | Load1; LoadLoad; Load2 | 在store2及其后的寫操作執(zhí)行前,保證store1的寫操作 |
StoreStore | Store1; StoreStore; Store2 | 在stroe2及其后的寫操作執(zhí)行前,保證load1的讀操作 |
StoreLoad | Store1; StoreLoad; Load2 | 保證store1的寫操作已刷新到主內(nèi)存之后,load2及其作 |
volatile內(nèi)存語(yǔ)義的實(shí)現(xiàn): JMM 針對(duì)編譯器制定的 volatile 重排序規(guī)則表
操作 | 普通讀寫 | volatile讀 | volatile寫 |
---|---|---|---|
普通讀寫 | 可以重排 | 可以重排 | 不可以重排 |
volatile讀 | 不可以重排 | 不可以重排 | 不可以重排 |
volatile寫 | 可以重排 | 不可以重排 | 不可以重排 |
比如第二行最后一個(gè)單元格的意思是:在程序中,當(dāng)?shù)谝粋€(gè)操作為普通變量的讀或?qū)憰r(shí),如果第二個(gè)操作為volatile寫,則編譯器不能重排序這兩個(gè)操作。
編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來(lái)禁止特定類型的處理器重排序:
- 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障
- 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障
class VolatileBarrierExample { int a; volatile int v1 = 1; volatile int v2 = 2; void readAndWrite() { int i = v1; // 第一個(gè)volatile讀、普通寫 int j = v2; // 第二個(gè)volatile讀、普通寫 a = i + j; // 普通寫 v1 = i + 1; // 第一個(gè)volatile寫 v2 = j * 2; // 第二個(gè) volatile寫 } }
4.2.MESI緩存一致性協(xié)議
鏈接: 認(rèn)識(shí)Java底層操作系統(tǒng)與并發(fā)基礎(chǔ). 多核CPU的情況下,如何保證緩存內(nèi)部數(shù)據(jù)的一致性?JAVA引入了MESI緩存一致性協(xié)議。
Java代碼的執(zhí)行流程:
volatile 修飾的變量(鎖也是)翻譯的匯編指令前會(huì)加 Lock 前綴,OS調(diào)度時(shí)會(huì) 觸發(fā)硬件緩存鎖定機(jī)制(總線鎖 或 緩存一致性協(xié)議) ,CPU 通過(guò)總線橋訪問(wèn)內(nèi)存條,多個(gè) CPU 訪問(wèn)同一內(nèi)存,首先需要拿到總線權(quán)。早期,計(jì)算機(jī)不發(fā)達(dá),性能低,總線鎖采用直接占有,其他 CPU 無(wú)法繼續(xù)通過(guò)總線橋訪問(wèn)。無(wú)法發(fā)揮 CPU 的多核能力?,F(xiàn)代 CPU 采用采用緩存一致性協(xié)議進(jìn)行保證(跨緩存行CacheLine(緩存存儲(chǔ)數(shù)據(jù)的數(shù)據(jù)單元) 時(shí)會(huì)升級(jí)為總線鎖)。
MESI 是指4種狀態(tài)的首字母。每個(gè) Cache line 有4個(gè)狀態(tài),可用2個(gè)bit表示:
狀態(tài) | 描述 | 監(jiān)聽任務(wù) |
---|---|---|
M 修改(Modified) | 該CacheLine有效,數(shù)據(jù)被修改了,和內(nèi)存中的數(shù)據(jù)不一致,數(shù)據(jù)只存在于本Cache中 | 緩存行必須時(shí)刻監(jiān)聽所有試圖讀該緩存行相對(duì)就主存的操作,這種操作必須在緩存將該緩存行寫回主存并將狀態(tài)變成S(共享)狀態(tài)之前被延遲執(zhí)行 |
E 獨(dú)享、互斥(Exclusive) | 該CacheLine有效,數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)一致,數(shù)據(jù)只存在于本Cache中 | 緩存行也必須監(jiān)聽其它緩存讀主存中該緩存行的操作,一旦有這種操作,該緩存行需要變成S(共享)狀態(tài) |
S 共享 (Shared) | 該CacheLine有效,數(shù)據(jù)和內(nèi)存中的數(shù)據(jù)一致,數(shù)據(jù)存在于很多Cache中 | 緩存行也必須監(jiān)聽其它緩存使該緩存行無(wú)效或者獨(dú)享該緩存行的請(qǐng)求,并將該緩存行變成無(wú)效(Invalid) |
I 無(wú)效 (Invalid) | 該CacheLine無(wú)效 | 無(wú) |
MESI 協(xié)議狀態(tài)切換過(guò)程分析:
舉例:
注意:一個(gè) CacheLine 裝不下變量,會(huì)升級(jí)為總線鎖。
MESI優(yōu)化和他們引入的問(wèn)題: 緩存的一致性消息傳遞是要時(shí)間的,這就使其切換時(shí)會(huì)產(chǎn)生延遲。當(dāng)一個(gè)緩存被切換狀態(tài)時(shí)其他緩存收到消息完成各自的切換并且發(fā)出回應(yīng)消息這么一長(zhǎng)串的時(shí)間中CPU都會(huì)等待所有緩存響應(yīng)完成??赡艹霈F(xiàn)的阻塞都會(huì)導(dǎo)致各種各樣的性能問(wèn)題和穩(wěn)定性問(wèn)題。
為了避免這種CPU運(yùn)算能力的浪費(fèi),Store Bufferes 被引入使用。處理器把它想要寫入到主存的值寫到緩存,然后繼續(xù)去處理其他事情。當(dāng)所有失效確認(rèn)(Invalidate Acknowledge)都接收到時(shí),數(shù)據(jù)才會(huì)最 終被提交。
但它也會(huì)帶來(lái)一定的風(fēng)險(xiǎn):
- 處理器會(huì)嘗試從存儲(chǔ)緩存(Store buffer)中讀取值,但它還沒(méi)有進(jìn)行提交。這個(gè)的解決方案稱為Store Forwarding,它使得加載的時(shí)候,如果存儲(chǔ)緩存中存在,則進(jìn)行返回
- 保存什么時(shí)候會(huì)完成,這個(gè)并沒(méi)有任何保證,可能會(huì)發(fā)生重排序(非指令重排)。CPU會(huì)讀到跟程序中寫入的順序不一樣的結(jié)果。
到此這篇關(guān)于Java內(nèi)存模型JMM與volatile的文章就介紹到這了,更多相關(guān)Java內(nèi)存模型內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java?中的?Lambda?List?轉(zhuǎn)?Map?的多種方法詳解
這篇文章主要介紹了Java中的Lambda?List轉(zhuǎn)Map幾種方式,傳統(tǒng)的方式又顯得太臃腫,于是就想到 Lambda 神器,今天我們就來(lái)看看都有哪幾種轉(zhuǎn)換方式(List -> Map),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友參考下吧2022-07-07java web response提供文件下載功能的實(shí)例講解
下面小編就為大家分享一篇java web response提供文件下載功能的實(shí)例講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-01-01springboot 集成redis哨兵主從的實(shí)現(xiàn)
本文主要介紹了springboot 集成redis哨兵主從的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07springboot項(xiàng)目配置多數(shù)據(jù)庫(kù)連接的示例詳解
這篇文章主要介紹了springboot項(xiàng)目配置多數(shù)據(jù)庫(kù)連接的示例,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2023-12-12詳解在springmvc中解決FastJson循環(huán)引用的問(wèn)題
本篇文章主要介紹了在springmvc中解決FastJson循環(huán)引用的問(wèn)題,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-01-01CentOS?7.9服務(wù)器Java部署環(huán)境配置的過(guò)程詳解
這篇文章主要介紹了CentOS?7.9服務(wù)器Java部署環(huán)境配置,主要包括ftp服務(wù)器搭建過(guò)程、jdk安裝方法以及mysql安裝過(guò)程,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07SpringBoot工程啟動(dòng)順序與自定義監(jiān)聽超詳細(xì)講解
這篇文章主要介紹了SpringBoot工程啟動(dòng)順序與自定義監(jiān)聽,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2022-11-11