JAVA偏向鎖的原理與實(shí)戰(zhàn)
1. 偏向鎖的核心原理
如果不存在線程競爭的一個(gè)線程獲得了鎖,那么鎖就進(jìn)入偏向狀態(tài),此時(shí)Mark Word的結(jié)構(gòu)變?yōu)槠蜴i結(jié)構(gòu),鎖對象的鎖標(biāo)志位(lock)被改為01,偏向標(biāo)志位(biased_lock)被改為1,然后線程的ID記錄在鎖對象的Mark Word中(使用CAS操作完成)。以后該線程獲取鎖時(shí)判斷一下線程ID和標(biāo)志位,就可以直接進(jìn)入同步塊,連CAS操作都不需要,這樣就省去了大量有關(guān)鎖申請的操作,從而也就提升了程序的性能。
關(guān)鍵點(diǎn):無競爭
缺點(diǎn):如果鎖對象時(shí)常被多個(gè)線程競爭,偏向鎖就是多余的,并且其撤銷的過程會(huì)帶來一些性能開銷
2. 偏向鎖代碼演示
偏向鎖是默認(rèn)是延遲的,不會(huì)在程序啟動(dòng)時(shí)立即生效,如果想避免延遲,可以加 VM 參數(shù)
-XX:BiasedLockingStartupDelay=0
來禁用延遲
package innerlock; import org.openjdk.jol.info.ClassLayout; import org.openjdk.jol.vm.VM; public class InnerLockTest { int a=1; double b=1.1; public static void main(String[] args) { System.out.println(VM.current().details()); Person person=new Person(); ClassLayout layout=ClassLayout.parseInstance(person); new Thread(()->{ System.out.println("獲取偏向鎖前:"); System.out.println(layout.toPrintable()); synchronized (person) { System.out.println("獲取偏向鎖中:"); System.out.println(layout.toPrintable()); } System.out.println("獲取偏向鎖結(jié)束后:"); System.out.println(layout.toPrintable()); } ,"thread1").start(); } } class Person{ }
禁用偏向鎖:添加 VM 參數(shù) -XX:-UseBiasedLocking
3. 偏向鎖的膨脹與撤銷
假如有多個(gè)線程來競爭偏向鎖,此對象鎖已經(jīng)有所偏向,其他的線程發(fā)現(xiàn)偏向鎖并不是偏向自己,就說明存在了競爭,嘗試撤銷偏向鎖(很可能引入安全點(diǎn)),然后膨脹到輕量級鎖
1. 偏向鎖的撤銷
1.在一個(gè)安全點(diǎn)停止擁有鎖的線程
2.遍歷線程的棧幀,檢查是否存在鎖記錄。如果存在鎖記錄,就需要清空鎖記錄,使其變成無鎖狀態(tài),并修復(fù)鎖記錄指向的Mark Word,清除其線程ID
3.將當(dāng)前鎖升級成輕量級鎖
4.喚醒當(dāng)前線程
撤銷偏向鎖的條件(滿足其一即可):
1.多個(gè)線程競爭偏向鎖
2.調(diào)用偏向鎖對象的hashcode()方法或者System.identityHashCode()方法計(jì)算對象的HashCode之后,將哈希碼放置到Mark Word中,內(nèi)置鎖變成無鎖狀態(tài),偏向鎖將被撤銷
2. 批量重偏向與撤銷
批量重偏向解決的問題:
一個(gè)線程創(chuàng)建了大量對象并執(zhí)行了初始的同步操作,之后在另一個(gè)線程中將這些對象作為鎖進(jìn)行之后的操作。這種case下,會(huì)導(dǎo)致大量的偏向鎖撤銷操作。
package innerlock; import java.util.ArrayList; import org.openjdk.jol.info.ClassLayout; import org.openjdk.jol.vm.VM; public class InnerLockTest { int a=1; double b=1.1; public static void main(String[] args) throws InterruptedException { System.out.println(VM.current().details()); ArrayList<Person> list=new ArrayList<Person>(); new Thread(()->{ for(int i=0;i<100;i++) { Person person=new Person(); synchronized (person) { list.add(person); } } try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } ,"thread1").start(); Thread.sleep(3000); new Thread(()->{ for(int i=0;i<30;i++) { Person person=list.get(i); synchronized (person) { if(i==17||i==18||i==19||i==21) { System.out.println("第"+(i+1)+"次偏向結(jié)果:"); System.out.println(ClassLayout.parseInstance(person).toPrintable()); } } } } ,"thread2").start(); } } class Person{ }
結(jié)果分析:
先用線程1創(chuàng)建了100個(gè)對象鎖,這些對象鎖都偏向于線程1,后面創(chuàng)建線程2去爭奪這些鎖,前19次線程2都是搶占失敗獲得輕量級鎖(失敗過程中閾值增加),第20次搶占時(shí)達(dá)到閾值20,這時(shí)JVM會(huì)認(rèn)為自己是不是不應(yīng)該偏向線程1,于是之后開始偏向線程2,線程2之后獲得的都是偏向鎖
- 第1-19個(gè)對象由于線程2在搶占過程中變?yōu)檩p量級鎖,鎖釋放后變?yōu)闊o鎖狀態(tài)
- 第20-30個(gè)對象觸發(fā)批量重定向,鎖釋放后依舊偏向線程2
- 第31-100個(gè)對象依然和開始一樣偏向線程1,鎖釋放后依舊偏向線程1
批量撤銷解決的問題:
存在明顯多線程競爭的場景下使用偏向鎖是不合適的,例如生產(chǎn)者/消費(fèi)者隊(duì)列
package innerlock; import java.util.ArrayList; import org.openjdk.jol.info.ClassLayout; import org.openjdk.jol.vm.VM; public class InnerLockTest { int a=1; double b=1.1; public static void main(String[] args) throws InterruptedException { System.out.println(VM.current().details()); ArrayList<Person> list=new ArrayList<Person>(); new Thread(()->{ for(int i=0;i<100;i++) { Person person=new Person(); synchronized (person) { list.add(person); } } try { //為了防止JVM線程復(fù)用,在創(chuàng)建完對象后,保持線程t1狀態(tài)為存活 Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } ,"thread1").start(); Thread.sleep(3000); new Thread(()->{ for(int i=0;i<40;i++) { Person person=list.get(i); synchronized (person) { if(i==18||i==19||i==39||i==41) { System.out.println("t2 第"+(i+1)+"次偏向結(jié)果:"); System.out.println(ClassLayout.parseInstance(person).toPrintable()); } } } try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } } ,"thread2").start(); Thread.sleep(3000); new Thread(()->{ for(int i=20;i<40;i++) { Person person=list.get(i); synchronized (person) { if(i==20||i==39) { System.out.println("t3 第"+(i+1)+"次偏向結(jié)果:"); System.out.println(ClassLayout.parseInstance(person).toPrintable()); } } } } ,"thread3").start(); Thread.sleep(1000); System.out.println("新創(chuàng)建對象:"+ClassLayout.parseInstance(new Person()).toPrintable()); } } class Person{ }
做法:
以class為單位,為每個(gè)class維護(hù)一個(gè)偏向鎖撤銷計(jì)數(shù)器,每一次該class的對象發(fā)生偏向撤銷操作時(shí),該計(jì)數(shù)器+1,當(dāng)這個(gè)值達(dá)到重偏向閾值默認(rèn)20時(shí),JVM就認(rèn)為該class的偏向鎖有問題,因此會(huì)進(jìn)行批量重偏向。每個(gè)class對象會(huì)有一個(gè)對應(yīng)的epoch字段,每個(gè)處于偏向鎖狀態(tài)對象的mark word中也有該字段,其初始值為創(chuàng)建該對象時(shí),class中的epoch的值。每次發(fā)生批量重偏向時(shí),就將該值+1,同時(shí)遍歷JVM中所有線程的棧,找到該class所有正處于加鎖狀態(tài)的偏向鎖,將其epoch字段改為新值。下次獲得鎖時(shí),發(fā)現(xiàn)當(dāng)前對象的epoch值和class的epoch不相等,那就算當(dāng)前已經(jīng)偏向了其他線程,也不會(huì)執(zhí)行撤銷操作,而是直接通過CAS操作將其mark word的Thread Id 改成當(dāng)前線程Id
當(dāng)達(dá)到重偏向閾值后,假設(shè)該class計(jì)數(shù)器繼續(xù)增長,當(dāng)其達(dá)到批量撤銷的閾值后(默認(rèn)40),JVM就認(rèn)為該class的使用場景存在多線程競爭,會(huì)標(biāo)記該class為不可偏向,之后,對于該class的鎖,直接走輕量級鎖的邏輯
小結(jié):
3. 偏向鎖的膨脹
如果偏向鎖被占據(jù),一旦有第二個(gè)線程爭搶這個(gè)對象,因?yàn)槠蜴i不會(huì)主動(dòng)釋放,所以第二個(gè)線程可以看到內(nèi)置鎖偏向狀態(tài),這時(shí)表明在這個(gè)對象鎖上已經(jīng)存在競爭了。JVM檢查原來持有該對象鎖的占有線程是否依然存活,如果掛了,就可以將對象變?yōu)闊o鎖狀態(tài),然后進(jìn)行重新偏向,偏向搶鎖線程。如果JVM檢查到原來的線程依然存活,就進(jìn)一步檢查占有線程的調(diào)用堆棧是否通過鎖記錄持有偏向鎖。如果存在鎖記錄,就表明原來的線程還在使用偏向鎖,發(fā)生鎖競爭,撤銷原來的偏向鎖,將偏向鎖膨脹(INFLATING)為輕量級鎖
總結(jié)
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關(guān)注腳本之家的更多內(nèi)容!
相關(guān)文章
java.lang.UnsupportedClassVersionError錯(cuò)誤的解決辦法(附圖文)
這篇文章主要給大家介紹了關(guān)于java.lang.UnsupportedClassVersionError錯(cuò)誤的解決辦法,"java.lang.UnsupportedClassVersionError"意味著您正在運(yùn)行的Java版本與編譯該類時(shí)使用的Java版本不兼容,需要的朋友可以參考下2023-10-10關(guān)于@PropertySource配置的用法解析
這篇文章主要介紹了關(guān)于@PropertySource配置的用法解析,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03Java Mybatis框架Dao層的實(shí)現(xiàn)與映射文件以及核心配置文件詳解分析
MyBatis 是一款優(yōu)秀的持久層框架,它支持自定義 SQL、存儲(chǔ)過程以及高級映射。MyBatis 免除了幾乎所有的 JDBC 代碼以及設(shè)置參數(shù)和獲取結(jié)果集的工作。MyBatis 可以通過簡單的 XML 或注解來配置和映射原始類型、接口和 Java POJO為數(shù)據(jù)庫中的記錄2021-10-10Java動(dòng)態(tài)規(guī)劃之丑數(shù)問題實(shí)例講解
這篇文章主要介紹了Java動(dòng)態(tài)規(guī)劃之丑數(shù)問題實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-09-09圖解Java經(jīng)典算法插入排序的原理與實(shí)現(xiàn)
插入排序的算法描述是一種簡單直觀的排序算法。其原理是通過構(gòu)建有序序列,對于未排序數(shù)據(jù),在已排序序列中從后向前掃描,找到相應(yīng)位置并插入。本文將用Java語言實(shí)現(xiàn)插入排序算法并進(jìn)行可視化,感興趣的可以了解一下2022-09-09深入分析JAVA 多線程--interrupt()和線程終止方式
這篇文章主要介紹了JAVA 多線程--interrupt()和線程終止方式的的相關(guān)資料,文中代碼非常細(xì)致,幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-06-06