淺談Java鎖的膨脹過(guò)程以及一致性哈希對(duì)鎖膨脹的影響
1、鎖優(yōu)化
在JDK6之前,通過(guò)synchronized來(lái)實(shí)現(xiàn)同步效率是很低的,被synchronized包裹的代碼塊經(jīng)過(guò)javac編譯后,會(huì)在代碼塊前后加上monitorenter
和monitorexit
字節(jié)碼指令,被synchronized修飾的方法則會(huì)被加上ACC_SYNCHRONIZED
標(biāo)識(shí),不論是在字節(jié)碼中如何表示,作用和功能都是一樣的,線程要想執(zhí)行同步代碼塊或同步方法,首先需要競(jìng)爭(zhēng)鎖。
synchronized保證了任意時(shí)刻最多只有一個(gè)線程可以競(jìng)爭(zhēng)到鎖,那么競(jìng)爭(zhēng)不到鎖的的線程該如何處理呢?
在JDK6之前,Java直接通過(guò)OS級(jí)別的互斥量(Mutex)來(lái)實(shí)現(xiàn)同步,獲取不到鎖的線程被阻塞掛起,直到持有鎖的線程釋放鎖后再將其喚醒,這需要OS頻繁的將線程從用戶態(tài)切換到核心態(tài),這個(gè)切換過(guò)程開銷是很大的,OS需要暫停原線程并保存數(shù)據(jù),喚醒新線程并恢復(fù)數(shù)據(jù),因此synchronized也被稱為“重量級(jí)鎖”。
也正是由于性能原因,開發(fā)者慢慢擯棄了synchronized,投入ReentrantLock
的懷抱。
官方意識(shí)到這個(gè)問(wèn)題以后,便將“高效并發(fā)”作為JDK6的一個(gè)重要改進(jìn)項(xiàng)目,經(jīng)過(guò)開發(fā)團(tuán)隊(duì)的重重優(yōu)化,如今synchronized的性能已經(jīng)和ReentrantLock保持在一個(gè)數(shù)量級(jí)了,雖然還是慢一丟丟,但是官方表示未來(lái)synchronized仍然有優(yōu)化的余地。
1.1、鎖消除
設(shè)計(jì)一個(gè)類時(shí),考慮到存在并發(fā)安全問(wèn)題,往往會(huì)對(duì)代碼塊上鎖。
但是有時(shí)候這個(gè)被設(shè)計(jì)為“線程安全”的類在使用時(shí)壓根就不存在多線程競(jìng)爭(zhēng),那么還有什么理由加鎖呢?
鎖消除優(yōu)化得益于逃逸分析技術(shù)的成熟,即時(shí)編譯器在運(yùn)行時(shí)會(huì)對(duì)代碼進(jìn)行掃描,會(huì)對(duì)不存在共享數(shù)據(jù)競(jìng)爭(zhēng)的鎖消除。
例如:在方法中(棧內(nèi)存線程私有)實(shí)例化一個(gè)線程安全的類,該實(shí)例既沒有傳遞給其他方法,又沒有作為對(duì)象返回出去(沒有發(fā)生逃逸),那么JVM就會(huì)對(duì)進(jìn)行鎖消除。
如下代碼,盡管StringBuffer的append()是被synchronized修飾的,但是不存在線程競(jìng)爭(zhēng),鎖會(huì)消除。
public String method(){ StringBuffer sb = new StringBuffer(); sb.append("1");//append()是被synchronized修飾的 sb.append("2"); return sb.toString(); }
1.2、鎖粗化
由于鎖的競(jìng)爭(zhēng)和釋放開銷比較大,如果代碼中對(duì)鎖進(jìn)行了頻繁的競(jìng)爭(zhēng)和釋放,那么JVM會(huì)進(jìn)行優(yōu)化,將鎖的范圍適當(dāng)擴(kuò)大。
如下代碼,在循環(huán)內(nèi)使用synchronized,JVM鎖粗化后,會(huì)將鎖范圍擴(kuò)大到循環(huán)外。
public void method(){ for (int i= 0; i < 100; i++) { synchronized (this){ ... } } }
1.3、自旋鎖
當(dāng)有多個(gè)線程在競(jìng)爭(zhēng)同一把鎖時(shí),競(jìng)爭(zhēng)失敗的線程如何處理?
兩種情況:
- 將線程掛起,鎖釋放后再將其喚醒。
- 線程不掛起,進(jìn)行自旋,直到競(jìng)爭(zhēng)成功。
如果鎖競(jìng)爭(zhēng)非常激烈,且短時(shí)間得不到釋放,那么將線程掛起效率會(huì)更高,因?yàn)楦?jìng)爭(zhēng)失敗的線程不斷自旋會(huì)造成CPU空轉(zhuǎn),浪費(fèi)性能。
如果鎖競(jìng)爭(zhēng)并不激烈,且鎖會(huì)很快得到釋放,那么自旋效率會(huì)更高。因?yàn)閷⒕€程掛起和喚醒是一個(gè)開銷很大的操作。
自旋鎖的優(yōu)化是針對(duì)“鎖競(jìng)爭(zhēng)不激烈,且會(huì)很快釋放”的場(chǎng)景,避免了OS頻繁掛起和喚醒線程。
1.4、自適應(yīng)自旋鎖
當(dāng)線程競(jìng)爭(zhēng)鎖失敗時(shí),自旋和掛起哪一種更高效?
當(dāng)線程競(jìng)爭(zhēng)鎖失敗時(shí),會(huì)自旋10次,如果仍然競(jìng)爭(zhēng)不到鎖,說(shuō)明鎖競(jìng)爭(zhēng)比較激烈,繼續(xù)自旋會(huì)浪費(fèi)性能,JVM就會(huì)將線程掛起。
在JDK6之前,自旋的次數(shù)通過(guò)JVM參數(shù)-XX:PreBlockSpin
設(shè)置,但是開發(fā)者往往不知道該設(shè)置多少比較合適,于是在JDK6中,對(duì)其進(jìn)行了優(yōu)化,加入了“自適應(yīng)自旋鎖”。
自適應(yīng)自旋鎖的大致原理:線程如果自旋成功了,那么下次自旋的最大次數(shù)會(huì)增加,因?yàn)镴VM認(rèn)為既然上次成功了,那么這一次也很大概率會(huì)成功。
反之,如果很少會(huì)自旋成功,那么下次會(huì)減少自旋的次數(shù)甚至不自旋,避免CPU空轉(zhuǎn)。
1.5、鎖膨脹
除了上述幾種優(yōu)化外,JDK6加入了新型的鎖機(jī)制,不直接采用OS級(jí)的“重量級(jí)鎖”,鎖類型分為:偏向鎖、輕量級(jí)鎖、重量級(jí)鎖。隨著鎖競(jìng)爭(zhēng)的激烈程度不斷膨脹,大大提升了競(jìng)爭(zhēng)不太激烈的同步性能。
“synchronized鎖的是對(duì)象,而非代碼!”
每一個(gè)Java對(duì)象,在JVM中是存在對(duì)象頭(Object Header)的,對(duì)象頭中又分Mark Word和Klass Pointer,其中Mark Word就保存了對(duì)象的鎖狀態(tài)信息,其結(jié)構(gòu)如下圖所示:
無(wú)鎖:初始狀態(tài)
一個(gè)對(duì)象被實(shí)例化后,如果還沒有被任何線程競(jìng)爭(zhēng)鎖,那么它就為無(wú)鎖狀態(tài)(01)。
偏向鎖:?jiǎn)尉€程競(jìng)爭(zhēng)
當(dāng)線程A第一次競(jìng)爭(zhēng)到鎖時(shí),通過(guò)CAS操作修改Mark Word中的偏向線程ID、偏向模式。如果不存在其他線程競(jìng)爭(zhēng),那么持有偏向鎖的線程將永遠(yuǎn)不需要進(jìn)行同步。
輕量級(jí)鎖:多線程競(jìng)爭(zhēng),但是任意時(shí)刻最多只有一個(gè)線程競(jìng)爭(zhēng)
如果線程B再去競(jìng)爭(zhēng)鎖,發(fā)現(xiàn)偏向線程ID不是自己,那么偏向模式就會(huì)立刻不可用。即使兩個(gè)線程不存在競(jìng)爭(zhēng)關(guān)系(線程A已經(jīng)釋放,線程B再去獲取),也會(huì)升級(jí)為輕量級(jí)鎖(00)。
重量級(jí)鎖:同一時(shí)刻多線程競(jìng)爭(zhēng)
一旦輕量級(jí)鎖CAS修改失敗,說(shuō)明存在多線程同時(shí)競(jìng)爭(zhēng)鎖,輕量級(jí)鎖就不適用了,必須膨脹為重量級(jí)鎖(10)。此時(shí)Mark Word存儲(chǔ)的就是指向重量級(jí)鎖(互斥量)的指針,后面等待鎖的線程必須進(jìn)入阻塞狀態(tài)。
2、鎖膨脹實(shí)戰(zhàn)
說(shuō)了這么多,理論終歸是理論,不如實(shí)戰(zhàn)一把來(lái)的直接。
通過(guò)編寫一些多線程競(jìng)爭(zhēng)代碼,以及打印對(duì)象的頭信息,來(lái)分析哪些情況下鎖會(huì)膨脹,以及膨脹成哪種類型的鎖。
2.1、jol工具
openjdk提供了jol工具,可以打印對(duì)象的內(nèi)存布局信息,依賴如下:
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version> </dependency>
2.2、鎖膨脹測(cè)試代碼
程序啟動(dòng)時(shí)先sleep5秒是為了等待偏向鎖系統(tǒng)啟動(dòng)。
編寫一段鎖逐步膨脹的測(cè)試代碼,如下所示:
public class LockTest { static class Lock{} public static void main(String[] args) { sleep(5000); Lock lock = new Lock(); System.err.println("無(wú)鎖"); print(lock); synchronized (lock) { //main線程首次競(jìng)爭(zhēng)鎖,可偏向 System.err.println("偏向鎖"); print(lock); } new Thread(()->{ synchronized (lock){ //線程A來(lái)競(jìng)爭(zhēng),偏向線程ID不是自己,升級(jí)為:輕量級(jí)鎖 System.err.println("輕量級(jí)鎖"); print(lock); } },"Thread-A").start(); sleep(2000); new Thread(()->{ synchronized (lock){ sleep(1000); } },"Thread-B").start(); //確保線程B啟動(dòng)并獲得鎖,sleep 100毫秒 sleep(100); synchronized (lock){ //main線程競(jìng)爭(zhēng)時(shí),線程B還未釋放,多線程同時(shí)競(jìng)爭(zhēng),升級(jí)為:重量級(jí)鎖 System.err.println("重量級(jí)鎖"); print(lock); } } static void print(Object o){ System.err.println("==========對(duì)象信息開始...=========="); System.out.println(ClassLayout.parseInstance(o).toPrintable()); //jol異步輸出,防止打印重疊,sleep1秒 sleep(1000); System.err.println("==========對(duì)象信息結(jié)束...=========="); } static void sleep(long l){ try { Thread.sleep(l); } catch (InterruptedException e) { e.printStackTrace(); } } }
2.3、輸出分析
運(yùn)行后分析一下控制臺(tái)輸出信息,這里貼上截圖并寫上注釋:
無(wú)鎖
偏向鎖
輕量級(jí)鎖
重量級(jí)鎖
以上,就是JVM中鎖逐步膨脹的過(guò)程,另外:鎖不支持回退撤銷。
2.4、鎖釋放
偏向鎖是不會(huì)主動(dòng)釋放的,只要沒有其他線程競(jìng)爭(zhēng),會(huì)永遠(yuǎn)偏向持有鎖的線程,這樣在以后的執(zhí)行中,都不用再進(jìn)行同步處理了,節(jié)省了同步開銷。
public static void main(String[] args) { sleep(5000); Lock lock = new Lock(); synchronized (lock){ System.err.println("Main線程首次競(jìng)爭(zhēng)鎖"); print(lock); } System.out.println(); sleep(1000); System.err.println("同步代碼塊退出以后"); print(lock); }
輕量級(jí)和重量級(jí)鎖均會(huì)主動(dòng)釋放,這里只貼出輕量級(jí)鎖。
public static void main(String[] args) { sleep(5000); Lock lock = new Lock(); synchronized (lock){ //偏向鎖 } new Thread(()->{ synchronized (lock){ System.err.println("輕量級(jí)鎖"); print(lock); } },"Thread-A").start(); sleep(5000); System.err.println("\n線程A釋放鎖后"); print(lock); }
重量級(jí)鎖類似,這里就不貼測(cè)試結(jié)果了。
3、一致性哈希對(duì)鎖膨脹的影響
一個(gè)對(duì)象如果計(jì)算過(guò)哈希碼,就應(yīng)該一直保持該值不變(強(qiáng)烈推薦但不強(qiáng)制,因?yàn)橛脩艨梢灾剌dhashCode()方法按自己的意愿返回哈希碼)。
在Java中,如果類沒有重寫hashCode(),那么會(huì)自動(dòng)繼承自O(shè)bject::hashCode(),Object::hashCode()就是一致性哈希,只要計(jì)算過(guò)一次,就會(huì)將哈希碼寫入到對(duì)象頭中,且永遠(yuǎn)不會(huì)改變。
和具體的哈希算法有關(guān),JVM里有五種哈希算法,通過(guò)參數(shù)
-XX:hashCode=[0|1|2|3|4]
指定。
只要對(duì)象計(jì)算過(guò)一致性哈希,偏向模式就置為0了,也就意味著該對(duì)象鎖不能再偏向了,最低也會(huì)膨脹會(huì)輕量級(jí)鎖。
如果對(duì)象鎖處于偏向模式時(shí)遇到計(jì)算一致性哈希請(qǐng)求,那么會(huì)跳過(guò)輕量級(jí)鎖模式,直接膨脹為重量級(jí)鎖。
鎖膨脹為輕量級(jí)或重量級(jí)鎖后,Mark Word中保存的分別是線程棧幀里的鎖記錄指針和重量級(jí)鎖指針,已經(jīng)沒有位置再保存哈希碼,GC年齡了,那么這些信息被移動(dòng)到哪里去了呢?
升級(jí)為輕量級(jí)鎖時(shí),JVM會(huì)在當(dāng)前線程的棧幀中創(chuàng)建一個(gè)鎖記錄(Lock Record)空間,用于存儲(chǔ)鎖對(duì)象的Mark Word拷貝,哈希碼和GC年齡自然保存在此,釋放鎖后會(huì)將這些信息寫回到對(duì)象頭。
升級(jí)為重量級(jí)鎖后,Mark Word保存的重量級(jí)鎖指針,代表重量級(jí)鎖的ObjectMonitor類里有字段記錄無(wú)鎖狀態(tài)下的Mark Word,鎖釋放后也會(huì)將信息寫回到對(duì)象頭。
代碼實(shí)戰(zhàn),跳過(guò)偏向鎖,直接膨脹輕量級(jí)鎖
public static void main(String[] args) { sleep(5000); Lock lock = new Lock(); //沒有重寫,一致性哈希,重寫后無(wú)效 lock.hashCode(); synchronized (lock){ System.err.println("本應(yīng)是偏向鎖,但是由于計(jì)算過(guò)一致性哈希,會(huì)直接膨脹為輕量級(jí)鎖"); print(lock); } }
偏向鎖過(guò)程中遇到一致性哈希計(jì)算請(qǐng)求,立馬撤銷偏向模式,膨脹為重量級(jí)鎖
public static void main(String[] args) { sleep(5000); Lock lock = new Lock(); synchronized (lock){ //沒有重寫,一致性哈希,重寫后無(wú)效 lock.hashCode(); System.err.println("偏向鎖過(guò)程中遇到一致性哈希計(jì)算請(qǐng)求,立馬撤銷偏向模式,膨脹為重量級(jí)鎖"); print(lock); } }
4、鎖性能測(cè)試
這里只做了一個(gè)簡(jiǎn)單的測(cè)試,實(shí)際應(yīng)用環(huán)境比測(cè)試環(huán)境要復(fù)雜的多。
單線程下,各類型鎖性能測(cè)試:
public class PerformanceTest { final static int TEST_COUNT = 100000000; static class Lock{} public static void main(String[] args) { sleep(5000); System.err.println("各類型鎖性能測(cè)試"); Lock lock = new Lock(); long start; long end; start = System.currentTimeMillis(); for (int i = 0; i < TEST_COUNT; i++) { } end = System.currentTimeMillis(); System.out.println("無(wú)鎖:" + (end - start)); //偏向鎖 biasedLock(lock); start = System.currentTimeMillis(); for (int i = 0; i < TEST_COUNT; i++) { synchronized (lock) {} } end = System.currentTimeMillis(); System.out.println("偏向鎖耗時(shí):" + (end - start)); //輕量級(jí)鎖 lightweightLock(lock); start = System.currentTimeMillis(); for (int i = 0; i < TEST_COUNT; i++) { synchronized (lock) {} } end = System.currentTimeMillis(); System.out.println("輕量級(jí)鎖耗時(shí):" + (end - start)); //重量級(jí)鎖 weightLock(lock); start = System.currentTimeMillis(); for (int i = 0; i < TEST_COUNT; i++) { synchronized (lock) {} } end = System.currentTimeMillis(); System.out.println("重量級(jí)鎖耗時(shí):" + (end - start)); } static void biasedLock(Object o){ synchronized (o){} } //將鎖升級(jí)為輕量級(jí) static void lightweightLock(Object o){ biasedLock(o); Thread thread = new Thread(() -> { synchronized (o) {} }); thread.start(); try { thread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } //將鎖升級(jí)為重量級(jí) static void weightLock(Object o){ lightweightLock(o); Thread t1 = new Thread(() -> { synchronized (o){ sleep(1000); } }); Thread t2 = new Thread(() -> { synchronized (o){ sleep(1000); } }); t1.start(); t2.start(); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } } static void sleep(long l){ try { Thread.sleep(l); } catch (InterruptedException e) { e.printStackTrace(); } } }
各類型鎖性能測(cè)試
無(wú)鎖:6
偏向鎖耗時(shí):252
輕量級(jí)鎖耗時(shí):2698
重量級(jí)鎖耗時(shí):1471
由于是單線程,不涉及鎖競(jìng)爭(zhēng),重量級(jí)鎖反而比輕量級(jí)鎖更快,因?yàn)椴恍枰狾S對(duì)線程進(jìn)行額外的調(diào)度,線程無(wú)需掛起和喚醒,而且不用拷貝Mark Word。
在多線程競(jìng)爭(zhēng)環(huán)境下,重量級(jí)鎖性能下降是毋庸置疑的,如下測(cè)試:
public static void main(String[] args) throws InterruptedException { System.err.println("多線程測(cè)試"); Lock lock = new Lock(); long start; long end; //輕量級(jí)鎖 lightweightLock(lock); start = System.currentTimeMillis(); for (int i = 0; i < TEST_COUNT; i++) { synchronized (lock) {} } end = System.currentTimeMillis(); System.out.println("輕量級(jí)鎖耗時(shí):" + (end - start)); //重量級(jí)鎖 weightLock(lock); Thread t1 = new Thread(() -> { for (int i = 0; i < TEST_COUNT / 2; i++) { synchronized (lock) {} } }); Thread t2 = new Thread(() -> { for (int i = 0; i < TEST_COUNT / 2; i++) { synchronized (lock) {} } }); t1.start(); t2.start(); start = System.currentTimeMillis(); t1.join(); t2.join(); end = System.currentTimeMillis(); System.out.println("重量級(jí)鎖耗時(shí):" + (end - start)); }
多線程測(cè)試
輕量級(jí)鎖耗時(shí):2581
重量級(jí)鎖耗時(shí):4460
實(shí)際的應(yīng)用環(huán)境遠(yuǎn)比測(cè)試環(huán)境復(fù)雜的多,鎖性能和線程競(jìng)爭(zhēng)的激烈程度、鎖占用的時(shí)間也有很大關(guān)系,測(cè)試結(jié)果僅供參考。
到此這篇關(guān)于淺談Java鎖的膨脹過(guò)程以及一致性哈希對(duì)鎖膨脹的影響的文章就介紹到這了,更多相關(guān)Java鎖膨脹內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
深入解析Java的Hibernate框架中的一對(duì)一關(guān)聯(lián)映射
這篇文章主要介紹了Java的Hibernate框架的一對(duì)一關(guān)聯(lián)映射,包括對(duì)一對(duì)一外聯(lián)映射的講解,需要的朋友可以參考下2016-01-01SpringBoot Shiro配置自定義密碼加密器代碼實(shí)例
這篇文章主要介紹了SpringBoot Shiro配置自定義密碼加密器代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-0325行Java代碼將普通圖片轉(zhuǎn)換為字符畫圖片和文本的實(shí)現(xiàn)
這篇文章主要介紹了25行Java代碼將普通圖片轉(zhuǎn)換為字符畫圖片和文本的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04Mybatis自關(guān)聯(lián)查詢一對(duì)多查詢的實(shí)現(xiàn)示例
這篇文章主要介紹了Mybatis自關(guān)聯(lián)查詢一對(duì)多查詢的實(shí)現(xiàn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02Java實(shí)現(xiàn)多線程輪流打印1-100的數(shù)字操作
這篇文章主要介紹了Java實(shí)現(xiàn)多線程輪流打印1-100的數(shù)字操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08