Java Synchronized鎖升級(jí)原理及過(guò)程剖析
前言
在上篇文章深入學(xué)習(xí)Synchronized各種使用方法當(dāng)中我們仔細(xì)介紹了在各種情況下該如何使用synchronized關(guān)鍵字。因?yàn)樵谖覀儗?xiě)的程序當(dāng)中可能會(huì)經(jīng)常使用到synchronized關(guān)鍵字,因此JVM對(duì)synchronized做出了很多優(yōu)化,而在本篇文章當(dāng)中我們將仔細(xì)介紹JVM對(duì)synchronized的各種優(yōu)化的細(xì)節(jié)。
工具準(zhǔn)備
在正式談synchronized的原理之前我們先談一下自旋鎖,因?yàn)樵趕ynchronized的優(yōu)化當(dāng)中自旋鎖發(fā)揮了很大的作用。而需要了解自旋鎖,我們首先需要了解什么是原子性。
所謂原子性簡(jiǎn)單說(shuō)來(lái)就是一個(gè)一個(gè)操作要么不做要么全做,全做的意思就是在操作的過(guò)程當(dāng)中不能夠被中斷,比如說(shuō)對(duì)變量data
進(jìn)行加一操作,有以下三個(gè)步驟:
- 將
data
從內(nèi)存加載到寄存器。 - 將
data
這個(gè)值加一。 - 將得到的結(jié)果寫(xiě)回內(nèi)存。
原子性就表示一個(gè)線程在進(jìn)行加一操作的時(shí)候,不能夠被其他線程中斷,只有這個(gè)線程執(zhí)行完這三個(gè)過(guò)程的時(shí)候其他線程才能夠操作數(shù)據(jù)data
。
我們現(xiàn)在用代碼體驗(yàn)一下,在Java當(dāng)中我們可以使用AtomicInteger
進(jìn)行對(duì)整型數(shù)據(jù)的原子操作:
import java.util.concurrent.atomic.AtomicInteger; public class AtomicDemo { public static void main(String[] args) throws InterruptedException { AtomicInteger data = new AtomicInteger(); data.set(0); // 將數(shù)據(jù)初始化位0 Thread t1 = new Thread(() -> { for (int i = 0; i < 100000; i++) { data.addAndGet(1); // 對(duì)數(shù)據(jù) data 進(jìn)行原子加1操作 } }); Thread t2 = new Thread(() -> { for (int i = 0; i < 100000; i++) { data.addAndGet(1);// 對(duì)數(shù)據(jù) data 進(jìn)行原子加1操作 } }); // 啟動(dòng)兩個(gè)線程 t1.start(); t2.start(); // 等待兩個(gè)線程執(zhí)行完成 t1.join(); t2.join(); // 打印最終的結(jié)果 System.out.println(data); // 200000 } }
從上面的代碼分析可以知道,如果是一般的整型變量如果兩個(gè)線程同時(shí)進(jìn)行操作的時(shí)候,最終的結(jié)果是會(huì)小于200000。
我們現(xiàn)在來(lái)模擬一下一般的整型變量出現(xiàn)問(wèn)題的過(guò)程:
主內(nèi)存data
的初始值等于0,兩個(gè)線程得到的data
初始值都等于0。
現(xiàn)在線程一將data
加一,然后線程一將data
的值同步回主內(nèi)存,整個(gè)內(nèi)存的數(shù)據(jù)變化如下:
現(xiàn)在線程二data
加一,然后將data
的值同步回主內(nèi)存(將原來(lái)主內(nèi)存的值覆蓋掉了):
我們本來(lái)希望data
的值在經(jīng)過(guò)上面的變化之后變成2
,但是線程二覆蓋了我們的值,因此在多線程情況下,會(huì)使得我們最終的結(jié)果變小。
但是在上面的程序當(dāng)中我們最終的輸出結(jié)果是等于20000的,這是因?yàn)榻odata
進(jìn)行+1
的操作是原子的不可分的,在操作的過(guò)程當(dāng)中其他線程是不能對(duì)data
進(jìn)行操作的。這就是原子性帶來(lái)的優(yōu)勢(shì)。
事實(shí)上上面的+1
原子操作就是通過(guò)自旋鎖實(shí)現(xiàn)的,我們可以看一下AtomicInteger
的源代碼:
public final int addAndGet(int delta) { // 在 AtomicInteger 內(nèi)部有一個(gè)整型數(shù)據(jù) value 用于存儲(chǔ)具體的數(shù)值的 // 這個(gè) valueOffset 表示這個(gè)數(shù)據(jù) value 在對(duì)象 this (也就是 AtomicInteger一個(gè)具體的對(duì)象) // 當(dāng)中的內(nèi)存偏移地址 // delta 就是我們需要往 value 上加的值 在這里我們加上的是 1 return unsafe.getAndAddInt(this, valueOffset, delta) + delta; }
上面的代碼最終是調(diào)用UnSafe
類的方法進(jìn)行實(shí)現(xiàn)的,我們?cè)倏匆幌滤脑创a:
public final int getAndAddInt(Object o, long offset, int delta) { int v; do { v = getIntVolatile(o, offset); // 從對(duì)象 o 偏移地址為 offset 的位置取出數(shù)據(jù) value ,也就是前面提到的存儲(chǔ)整型數(shù)據(jù)的變量 } while (!compareAndSwapInt(o, offset, v, v + delta)); return v; }
上面的代碼主要流程是不斷的從內(nèi)存當(dāng)中取對(duì)象內(nèi)偏移地址為offset
的數(shù)據(jù),然后執(zhí)行語(yǔ)句!compareAndSwapInt(o, offset, v, v + delta)
這條語(yǔ)句的主要作用是:比較對(duì)象o
內(nèi)存偏移地址為offset
的數(shù)據(jù)是否等于v
,如果等于v
則將偏移地址為offset
的數(shù)據(jù)設(shè)置為v + delta
,如果這條語(yǔ)句執(zhí)行成功返回 true
否則返回false
,這就是我們常說(shuō)的Java當(dāng)中的CAS。
看到這里你應(yīng)該就發(fā)現(xiàn)了當(dāng)上面的那條語(yǔ)句執(zhí)行不成功的話就會(huì)一直進(jìn)行while循環(huán)操作,直到操作成功之后才退出while循環(huán),假如沒(méi)有操作成功就會(huì)一直“旋”在這里,像這種操作就是自旋,通過(guò)這種自旋方式所構(gòu)成的鎖就叫做自旋鎖。
對(duì)象的內(nèi)存布局
在JVM當(dāng)中,一個(gè)Java對(duì)象的內(nèi)存主要有三塊:
- 對(duì)象頭,對(duì)象頭包含兩部分?jǐn)?shù)據(jù),分別是Mark word和類型指針(Kclass pointer)。
- 實(shí)例數(shù)據(jù),就是我們?cè)陬惍?dāng)中定義的各種數(shù)據(jù)。
- 對(duì)齊填充,JVM在實(shí)現(xiàn)的時(shí)候要求每一個(gè)對(duì)象所占有的內(nèi)存大小都需要是8字節(jié)的整數(shù)倍,如果一個(gè)對(duì)象的數(shù)據(jù)所占有的內(nèi)存大小不夠8字節(jié)的整數(shù)倍,那就需要進(jìn)行填充,補(bǔ)齊到8字節(jié),比如說(shuō)如果一個(gè)對(duì)象站60字節(jié),那么最終會(huì)填充到64字節(jié)。
而與我們要談到的synchronized鎖升級(jí)原理密切相關(guān)的是Mark word,這個(gè)字段主要是存儲(chǔ)對(duì)象運(yùn)行時(shí)的數(shù)據(jù),比如說(shuō)對(duì)象的Hashcode、GC的分代年齡、持有鎖的線程等等。而Kclass pointer主要是用于指向?qū)ο蟮念?,主要是表示這個(gè)對(duì)象是屬于哪一個(gè)類,主要是尋找類的元數(shù)據(jù)。
在32位Java虛擬機(jī)當(dāng)中Mark word有4個(gè)字節(jié)一共32個(gè)比特位,其內(nèi)容如下:
我們?cè)谑褂胹ynchronized時(shí),如果我們是將synchronized用在同步代碼塊,我們需要一個(gè)鎖對(duì)象。對(duì)于這個(gè)鎖對(duì)象來(lái)說(shuō)一開(kāi)始還沒(méi)有線程執(zhí)行到同步代碼塊時(shí),這個(gè)4個(gè)字節(jié)的內(nèi)容如上圖所示,其中有25個(gè)比特用來(lái)存儲(chǔ)哈希值,4個(gè)比特用來(lái)存儲(chǔ)垃圾回收的分代年齡(如果不了解可以跳過(guò)),剩下三個(gè)比特其中第一個(gè)用來(lái)表示當(dāng)前的鎖狀態(tài)是否為偏向鎖,最后的兩個(gè)比特表示當(dāng)前的鎖是哪一種狀態(tài):
- 如果最后三個(gè)比特是:001,則說(shuō)明鎖狀態(tài)是沒(méi)有鎖。
- 如果最后三個(gè)比特是:101,則說(shuō)明鎖狀態(tài)是偏向鎖。
- 如果最后兩個(gè)比特是:00, 則說(shuō)明鎖狀態(tài)是輕量級(jí)鎖。
- 如果最后兩個(gè)比特是:10, 則說(shuō)明鎖狀態(tài)是重量級(jí)鎖。
而synchronized鎖升級(jí)的順序是:無(wú)??->偏向??->輕量級(jí)??->重量級(jí)??。
在Java當(dāng)中有一個(gè)JVM參數(shù)用于設(shè)置在JVM啟動(dòng)多少秒之后開(kāi)啟偏向鎖(JDK6之后默認(rèn)開(kāi)啟偏向鎖,JVM默認(rèn)啟動(dòng)4秒之后開(kāi)啟對(duì)象偏向鎖,這個(gè)延遲時(shí)間叫做偏向延遲,你可以通過(guò)下面的參數(shù)進(jìn)行控制):
//設(shè)置偏向延遲時(shí)間 只有經(jīng)過(guò)這個(gè)時(shí)間只有對(duì)象鎖才會(huì)有偏向鎖這個(gè)狀態(tài) -XX:BiasedLockingStartupDelay=4 //禁止偏向鎖 -XX:-UseBiasedLocking //開(kāi)啟偏向鎖 -XX:+UseBiasedLocking
我們可以用代碼驗(yàn)證一下在無(wú)鎖狀態(tài)下,MarkWord的內(nèi)容是什么:
import org.openjdk.jol.info.ClassLayout; import java.util.concurrent.TimeUnit; public class MarkWord { public Object o = new Object(); public synchronized void demo() { synchronized (o) { System.out.println("synchronized代碼塊內(nèi)"); System.out.println(ClassLayout.parseInstance(o).toPrintable()); } } public static void main(String[] args) throws InterruptedException { System.out.println("等待4s前"); System.out.println(ClassLayout.parseInstance(new Object()).toPrintable()); TimeUnit.SECONDS.sleep(4); MarkWord markWord = new MarkWord(); System.out.println("等待4s后"); System.out.println(ClassLayout.parseInstance(new Object()).toPrintable()); Thread thread = new Thread(markWord::demo); thread.start(); thread.join(); System.out.println(ClassLayout.parseInstance(markWord.o).toPrintable()); } }
上面代碼輸出結(jié)果,下面的紅框框住的表示是否是偏向鎖和鎖標(biāo)志位(可能你會(huì)有疑問(wèn)為什么是這個(gè)位置,不應(yīng)該是最后3個(gè)比特位表示鎖相關(guān)的狀態(tài)嗎,這個(gè)其實(shí)是數(shù)據(jù)表示的大小端問(wèn)題,大家感興趣可以去查一下,在這你只需知道紅框三個(gè)比特就是用于表示是否為偏向鎖和鎖的標(biāo)志位):
從上面的圖當(dāng)中我們可以分析得知在偏向延遲的時(shí)間之前,對(duì)象鎖的狀態(tài)還不會(huì)有偏向鎖,因此對(duì)象頭中的Markword當(dāng)中鎖狀態(tài)是01,同時(shí)偏向鎖狀態(tài)是0,表示這個(gè)時(shí)候是無(wú)鎖狀態(tài),但是在4秒之后偏向鎖的狀態(tài)已經(jīng)變成1了,因此當(dāng)前的鎖狀態(tài)是偏向鎖,但是還沒(méi)有線程占有他,這種狀態(tài)也被稱作匿名偏向,因?yàn)樵谏厦娴拇a當(dāng)中只有一個(gè)線程進(jìn)入了synchronized同步代碼塊,因此可以使用偏向鎖,因此在synchronized代碼塊當(dāng)中打印的對(duì)象的鎖狀態(tài)也是偏向鎖。
上面的代碼當(dāng)中使用到了jol包,你需要在你的pom文件當(dāng)中引入對(duì)應(yīng)的包:
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.10</version> </dependency>
上圖當(dāng)中我們顯示的結(jié)果是在64位機(jī)器下面顯示的結(jié)果,在64位機(jī)器當(dāng)中在Java對(duì)象頭當(dāng)中的MarkWord和Klcass Pointer內(nèi)存布局如下:
其中MarkWord占8個(gè)字節(jié),Kclass Pointer占4個(gè)字節(jié)。JVM在64位和32位機(jī)器上的MarkWord內(nèi)容基本一致,64位機(jī)器上和32位機(jī)器上的MarkWord內(nèi)容和表示意義是一樣的,因此最后三位的意義你可以參考32位JVM的MarkWord。
鎖升級(jí)過(guò)程
偏向鎖
假如你寫(xiě)的synchronized代碼塊沒(méi)有多個(gè)線程執(zhí)行,而只有一個(gè)線程執(zhí)行的時(shí)候這種鎖對(duì)程序性能的提高還是非常大的。他的具體做法是JVM會(huì)將對(duì)象頭當(dāng)中的第三個(gè)用于表示是否為偏向鎖的比特位設(shè)置為1,同時(shí)會(huì)使用CAS操作將線程的ID記錄到Mark Word當(dāng)中,如果操作成功就相當(dāng)于獲得??了,那么下次這個(gè)線程想進(jìn)入臨界區(qū)就只需要比較一下線程ID是否相同了,而不需要進(jìn)行CAS或者加鎖這樣花費(fèi)比較大的操作了,只需要進(jìn)行一個(gè)簡(jiǎn)單的比較即可,這種情況下加鎖的開(kāi)銷非常小。
可能你會(huì)有一個(gè)疑問(wèn)在無(wú)鎖的狀態(tài)下Mark Word存儲(chǔ)的是哈希值,而在偏向鎖的狀態(tài)下存儲(chǔ)的是線程的ID,那么之前存儲(chǔ)的Hash Code不就沒(méi)有了嘛!你可能會(huì)想沒(méi)有就沒(méi)有吧,再算一遍不就行了!事實(shí)上不是這樣,如果我們計(jì)算過(guò)哈希值之后我們需要盡量保持哈希值不變(但是這個(gè)在Java當(dāng)中并沒(méi)有強(qiáng)制,因?yàn)樵贘ava當(dāng)中可以重寫(xiě)hashCode方法),因此在Java當(dāng)中為了能夠保持哈希值的不變性就會(huì)在第一次計(jì)算一致性哈希值(Mark Word里面存儲(chǔ)的是一致性哈希值,并不是指重寫(xiě)的hashCode返回值,在Java當(dāng)中可以通過(guò) Object.hashCode()或者System.identityHashCode(Object)方法計(jì)算一致性哈希值)的時(shí)候就將計(jì)算出來(lái)的一致性哈希值存儲(chǔ)到Mark Word當(dāng)中,下一次再有一致性哈希值的請(qǐng)求的時(shí)候就將存儲(chǔ)下來(lái)的一致性哈希值返回,這樣就可以保證每次計(jì)算的一致性哈希值相同。但是在變成偏向鎖的時(shí)候會(huì)使用線程ID覆蓋哈希值,因此當(dāng)一個(gè)對(duì)象計(jì)算過(guò)一致性哈希值之后,他就再也不能進(jìn)行偏向鎖狀態(tài),而且當(dāng)一個(gè)對(duì)象正處于偏向鎖狀態(tài)的時(shí)候,收到了一致性哈希值的請(qǐng)求的時(shí)候,也就是調(diào)用上面提到的兩個(gè)方法,偏向鎖就會(huì)立馬膨脹為重量級(jí)鎖,然后將Mark Word 儲(chǔ)在重量級(jí)鎖里。
下面的代碼就是驗(yàn)證當(dāng)在偏向鎖的狀態(tài)調(diào)用System.identityHashCode
函數(shù)鎖的狀態(tài)就會(huì)升級(jí)為重量級(jí)鎖:
import org.openjdk.jol.info.ClassLayout; import java.util.concurrent.TimeUnit; public class MarkWord { public Object o = new Object(); public synchronized void demo() { System.out.println("System.identityHashCode(o) 函數(shù)之前"); System.out.println(ClassLayout.parseInstance(o).toPrintable()); synchronized (o) { System.identityHashCode(o); System.out.println("System.identityHashCode(o) 函數(shù)之后"); System.out.println(ClassLayout.parseInstance(o).toPrintable()); } } public static void main(String[] args) throws InterruptedException { TimeUnit.SECONDS.sleep(5); MarkWord markWord = new MarkWord(); Thread thread = new Thread(markWord::demo); thread.start(); thread.join(); TimeUnit.SECONDS.sleep(2); System.out.println(ClassLayout.parseInstance(markWord.o).toPrintable()); } }
輕量級(jí)鎖
輕量級(jí)鎖也是在JDK1.6加入的,當(dāng)一個(gè)線程獲取偏向鎖的時(shí)候,有另外的線程加入鎖的競(jìng)爭(zhēng)時(shí),這個(gè)時(shí)候就會(huì)從偏向鎖升級(jí)為輕量級(jí)鎖。
在輕量級(jí)鎖的狀態(tài)時(shí),虛擬機(jī)首先會(huì)在當(dāng)前線程的棧幀當(dāng)中建立一個(gè)鎖記錄(Lock Record),用于存儲(chǔ)對(duì)象MarkWord的拷貝,官方稱這個(gè)為Displaced Mark Word。然后虛擬機(jī)會(huì)使用CAS操作嘗試將對(duì)象的MarkWord指向棧中的Lock Record,如果操作成功說(shuō)明這個(gè)線程獲取到了鎖,能夠進(jìn)入同步代碼塊執(zhí)行,否則說(shuō)明這個(gè)鎖對(duì)象已經(jīng)被其他線程占用了,線程就需要使用CAS不斷的進(jìn)行獲取鎖的操作,當(dāng)然你可能會(huì)有疑問(wèn),難道就讓線程一直死循環(huán)了嗎?這對(duì)CPU的花費(fèi)那不是太大了嗎,確實(shí)是這樣的因此在CAS滿足一定條件的時(shí)候輕量級(jí)鎖就會(huì)升級(jí)為重量級(jí)鎖,具體過(guò)程在重量級(jí)鎖章節(jié)中分析。
當(dāng)線程需要從同步代碼塊出來(lái)的時(shí)候,線程同樣的需要使用CAS將Displaced Mark Word替換回對(duì)象的MarkWord,如果替換成功,那么同步過(guò)程就完成了,如果替換失敗就說(shuō)明有其他線程嘗試獲取該鎖,而且鎖已經(jīng)升級(jí)為重量級(jí)鎖,此前競(jìng)爭(zhēng)鎖的線程已經(jīng)被掛起,因此線程在釋放鎖的同時(shí)還需要將掛起的線程喚醒。
重量級(jí)鎖
所謂重量級(jí)鎖就是一種開(kāi)銷最大的鎖機(jī)制,在這種情況下需要操作系統(tǒng)將沒(méi)有進(jìn)入同步代碼塊的線程掛起,JVM(Linux操作系統(tǒng)下)底層是使用pthread_mutex_lock
、pthread_mutex_unlock
、pthread_cond_wait
、pthread_cond_signal
和pthread_cond_broadcast
這幾個(gè)庫(kù)函數(shù)實(shí)現(xiàn)的,而這些函數(shù)依賴于futex
系統(tǒng)調(diào)用,因此在使用重量級(jí)鎖的時(shí)候因?yàn)檫M(jìn)行了系統(tǒng)調(diào)用,進(jìn)程需要從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài)將線程掛起,然后從內(nèi)核態(tài)轉(zhuǎn)為用戶態(tài),當(dāng)解鎖的時(shí)候又需要從用戶態(tài)轉(zhuǎn)為內(nèi)核態(tài)將線程喚醒,這一來(lái)二去的花費(fèi)就比較大了(和CAS自旋鎖相比)。
在有兩個(gè)以上的線程競(jìng)爭(zhēng)同一個(gè)輕量級(jí)鎖的情況下,輕量級(jí)鎖不再有效(輕量級(jí)鎖升級(jí)的一個(gè)條件),這個(gè)時(shí)候鎖為膨脹成重量級(jí)鎖,鎖的標(biāo)志狀態(tài)變成10,MarkWord當(dāng)中存儲(chǔ)的就是指向重量級(jí)鎖的指針,后面等待鎖的線程就會(huì)被掛起。
因?yàn)檫@個(gè)時(shí)候MarkWord當(dāng)中存儲(chǔ)的已經(jīng)是指向重量級(jí)鎖的指針,因此在輕量級(jí)鎖的情況下進(jìn)入到同步代碼塊在出同步代碼塊的時(shí)候使用CAS將Displaced Mark Word替換回對(duì)象的MarkWord的時(shí)候就會(huì)替換失敗,在前文已經(jīng)提到,在失敗的情況下,線程在釋放鎖的同時(shí)還需要將被掛起的線程喚醒。
總結(jié)
在本篇文章當(dāng)中我們主要介紹了synchronized內(nèi)部鎖升級(jí)的原理,具體的鎖升級(jí)的過(guò)程是:無(wú)??->偏向??->輕量級(jí)??->重量級(jí)??。
無(wú)鎖:這是沒(méi)有開(kāi)啟偏向鎖的時(shí)候的狀態(tài),在JDK1.6之后偏向鎖的默認(rèn)開(kāi)啟的,但是有一個(gè)偏向延遲,需要在JVM啟動(dòng)之后的多少秒之后才能開(kāi)啟,這個(gè)可以通過(guò)JVM參數(shù)進(jìn)行設(shè)置,同時(shí)是否開(kāi)啟偏向鎖也可以通過(guò)JVM參數(shù)設(shè)置。
偏向鎖:這個(gè)是在偏向鎖開(kāi)啟之后的鎖的狀態(tài),如果還沒(méi)有一個(gè)線程拿到這個(gè)鎖的話,這個(gè)狀態(tài)叫做匿名偏向,當(dāng)一個(gè)線程拿到偏向鎖的時(shí)候,下次想要競(jìng)爭(zhēng)鎖只需要拿線程ID跟MarkWord當(dāng)中存儲(chǔ)的線程ID進(jìn)行比較,如果線程ID相同則直接獲取鎖(相當(dāng)于鎖偏向于這個(gè)線程),不需要進(jìn)行CAS操作和將線程掛起的操作。
輕量級(jí)鎖:在這個(gè)狀態(tài)下線程主要是通過(guò)CAS操作實(shí)現(xiàn)的。將對(duì)象的MarkWord存儲(chǔ)到線程的虛擬機(jī)棧上,然后通過(guò)CAS將對(duì)象的MarkWord的內(nèi)容設(shè)置為指向Displaced Mark Word的指針,如果設(shè)置成功則獲取鎖。在線程出臨界區(qū)的時(shí)候,也需要使用CAS,如果使用CAS替換成功則同步成功,如果失敗表示有其他線程在獲取鎖,那么就需要在釋放鎖之后將被掛起的線程喚醒。
重量級(jí)鎖:當(dāng)有兩個(gè)以上的線程獲取鎖的時(shí)候輕量級(jí)鎖就會(huì)升級(jí)為重量級(jí)鎖,因?yàn)镃AS如果沒(méi)有成功的話始終都在自旋,進(jìn)行while循環(huán)操作,這是非常消耗CPU的,但是在升級(jí)為重量級(jí)鎖之后,線程會(huì)被操作系統(tǒng)調(diào)度然后掛起,這可以節(jié)約CPU資源。
到此這篇關(guān)于Java Synchronized鎖升級(jí)原理及過(guò)程剖析的文章就介紹到這了,更多相關(guān)Synchronized鎖升級(jí)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java大話之創(chuàng)建型設(shè)計(jì)模式教程示例
這篇文章主要為大家介紹了java大話之創(chuàng)建型設(shè)計(jì)模式教程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02Java9版本新特性同一個(gè)Jar支持多JDK版本運(yùn)行
這篇文章主要為大家介紹了Java9新版本的特性之同一個(gè)Jar支持多JDK版本運(yùn)行的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03Java畢業(yè)設(shè)計(jì)實(shí)戰(zhàn)之工作管理系統(tǒng)的實(shí)現(xiàn)
這是一個(gè)使用了java+SSM+Jsp+Mysql開(kāi)發(fā)的工作干活管理系統(tǒng),是一個(gè)畢業(yè)設(shè)計(jì)的實(shí)戰(zhàn)練習(xí),具有管理系統(tǒng)該有的所有功能,感興趣的朋友快來(lái)看看吧2022-02-02簡(jiǎn)單了解Spring Cloud搭建Config過(guò)程實(shí)例
這篇文章主要介紹了簡(jiǎn)單了解Spring Cloud搭建Config過(guò)程實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12Java向上轉(zhuǎn)型和向下轉(zhuǎn)型實(shí)例解析
這篇文章主要介紹了Java向上轉(zhuǎn)型和向下轉(zhuǎn)型實(shí)例解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02簡(jiǎn)單實(shí)現(xiàn)Java通訊錄系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了如何簡(jiǎn)單實(shí)現(xiàn)Java通訊錄系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-02-02springboot項(xiàng)目中添加自定義日志及配置過(guò)程
這篇文章主要介紹了springboot項(xiàng)目中添加自定義日志,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-07-07