深入講解Java?synchronized的核心原理
前言
在此之前先有幾個面試題,看大家能答對幾題
1.1: 標(biāo)準(zhǔn)訪問ab二個線程,是先打印t1還是t2
public class SyncUnit { public synchronized void t1() { System.out.println("t1"); } public synchronized void t2() { System.out.println("t2"); } public static void main(String[] args) throws Exception{ SyncUnit syncUnit = new SyncUnit(); new Thread(() -> { syncUnit.t1(); }).start(); Thread.sleep(100); new Thread(() -> { syncUnit.t2(); }).start(); } }
1.2: t1方法暫停3秒鐘,是先打印t1還是t2
public class SyncUnit { public synchronized void t1(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1"); } public synchronized void t2() { System.out.println("t2"); } public static void main(String[] args) throws Exception{ SyncUnit syncUnit = new SyncUnit(); new Thread(() -> { syncUnit.t1(); }).start(); Thread.sleep(100); new Thread(() -> { syncUnit.t2(); }).start(); }
1.3: 新增一個普通方法hello(),是先打印t1還是hello
public class SyncUnit { public synchronized void t1(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1"); } public synchronized void t2() { System.out.println("t2"); } public void hello() { System.out.println("hello"); } public static void main(String[] args) throws Exception{ SyncUnit syncUnit = new SyncUnit(); new Thread(() -> { syncUnit.t1(); }).start(); Thread.sleep(100); new Thread(() -> { syncUnit.hello(); }).start(); } }
1.4: 現(xiàn)在有二個SyncUnit對象,是先打印t1還是t2
public class SyncUnit { public synchronized void t1(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1"); } public synchronized void t2() { System.out.println("t2"); } public static void main(String[] args) throws Exception{ SyncUnit syncUnit = new SyncUnit(); SyncUnit syncUnit1 = new SyncUnit(); new Thread(() -> { syncUnit.t1(); }).start(); Thread.sleep(100); new Thread(() -> { syncUnit1.t2(); }).start(); }
1.5: 二個靜態(tài)同步方法,一個SuncUnit對象,是先打印t1還是t2
public class SyncUnit { public static synchronized void t1(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1"); } public static synchronized void t2() { System.out.println("t2"); } public static void main(String[] args) throws Exception{ SyncUnit syncUnit = new SyncUnit(); new Thread(() -> { syncUnit.t1(); }).start(); Thread.sleep(100); new Thread(() -> { syncUnit.t2(); }).start(); } }
1.6: 二個靜態(tài)同步方法,二個SyncUnit對象,是先打印t1還是t2
public class SyncUnit { public static synchronized void t1(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1"); } public static synchronized void t2() { System.out.println("t2"); } public static void main(String[] args) throws Exception{ SyncUnit syncUnit = new SyncUnit(); SyncUnit syncUnit1 = new SyncUnit(); new Thread(() -> { syncUnit.t1(); }).start(); Thread.sleep(100); new Thread(() -> { syncUnit1.t2(); }).start(); } }
1.7: 一個靜態(tài)同步方法,普通同步方法,一個SyncUnit對象,是先打印t1還是t2
public class SyncUnit { public static synchronized void t1(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1"); } public synchronized void t2() { System.out.println("t2"); } public static void main(String[] args) throws Exception{ SyncUnit syncUnit = new SyncUnit(); new Thread(() -> { syncUnit.t1(); }).start(); Thread.sleep(100); new Thread(() -> { syncUnit.t2(); }).start(); } }
1.8 一個靜態(tài)同步方法,普通同步方法,二個SyncUnit對象,是先打印t1還是t2
public class SyncUnit { public static synchronized void t1(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1"); } public synchronized void t2() { System.out.println("t2"); } public static void main(String[] args) throws Exception{ SyncUnit syncUnit = new SyncUnit(); SyncUnit syncUnit1 = new SyncUnit(); new Thread(() -> { syncUnit.t1(); }).start(); Thread.sleep(100); new Thread(() -> { syncUnit1.t2(); }).start(); } }
synchronized用法
synchronized是java提供的一種解決多線程并發(fā)安全的一種內(nèi)置鎖,盡管在jdk1.5之前還被大家吐槽性能問題,但是在1.5之后對synchronized不斷的優(yōu)化,在單機(jī)程序中,當(dāng)設(shè)計(jì)多線程并發(fā)問題時,我們完全可以使用synchronized解決
同步實(shí)例方法
public synchronized void method() { //方法邏輯 }
當(dāng)synchronized修飾的是一個普通方法的時候,相當(dāng)于對this對象加鎖,一個實(shí)例是可以創(chuàng)建多個對象的,所以可以擁有多把鎖,就比如下面這個例子,我們創(chuàng)建了二個對象,那就是二把不同的鎖,所以在調(diào)用t1()的時候,t2()方法由于是不同的鎖,所以會直接執(zhí)行方法
public class SyncUnit { public synchronized void t1(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1"); } public synchronized void t2() { System.out.println("t2"); } public static void main(String[] args) throws Exception{ SyncUnit syncUnit = new SyncUnit(); SyncUnit syncUnit1 = new SyncUnit(); new Thread(() -> { syncUnit.t1(); }).start(); Thread.sleep(100); new Thread(() -> { syncUnit1.t2(); }).start(); } }
同步靜態(tài)方法
public static synchronized void method() { //方法邏輯 }
當(dāng)synchronized修飾的是一個靜態(tài)方法的時候,相當(dāng)于對當(dāng)前實(shí)例加鎖,一個類只有一個實(shí)例,所以無論你創(chuàng)建多少個對象,都只有一把鎖,比如下面這個例子,雖然創(chuàng)建了二個不同的對象,但是實(shí)際只有一把鎖,所以是先打印t1(),然后在打印t2(),因?yàn)閠2()要等待t1()把鎖釋放掉之后才能獲取到鎖
public class SyncUnit { public static synchronized void t1(){ try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("t1"); } public static synchronized void t2() { System.out.println("t2"); } public static void main(String[] args) throws Exception{ SyncUnit syncUnit = new SyncUnit(); SyncUnit syncUnit1 = new SyncUnit(); new Thread(() -> { syncUnit.t1(); }).start(); Thread.sleep(100); new Thread(() -> { syncUnit1.t2(); }).start(); } }
代碼塊
public Object object = new Object(); public void method() { synchronized(object) { //方法邏輯 } }
這時候的object是一個對象,就相當(dāng)于在普通方法上添加synchronized,如果是不同的對象,那么就是不同的鎖
public void method() { synchronized(Test.class) { //方法邏輯 } }
這時候就相當(dāng)于在靜態(tài)方法上添加synchronized,也就是對當(dāng)前實(shí)例加鎖,一個類只有一個實(shí)例
synchronized核心原理
synchornized是基于JVM中的Monitor鎖實(shí)現(xiàn)的,Java1.5版本之前的synchornized鎖性能較低,但是從1.6版本之后,對synchornized進(jìn)行了大量的優(yōu)化,引入了鎖粗化,鎖消除,偏向鎖,輕量級鎖,適應(yīng)性自旋等技術(shù)來提升synchornized的性能
1.synchornized修飾的是方法
當(dāng)synchornized修飾的是方法的時候,當(dāng)前方法會比普通方法多一個ACC_SYNCHRONIZED的標(biāo)識符
當(dāng)JVM執(zhí)行程序的時候,會判斷這個方法是否有ACC_SYNCHRONIZED這個標(biāo)識符,如果有,則當(dāng)前線程優(yōu)先獲取Monitor對象,同一個時刻只能有一個線程獲取到,在當(dāng)前線程釋放Monitor對象之前,其它線程無法獲取到同一個Monitor對象,從而保證了同一時刻只能有一個線程進(jìn)入到被synchornized修飾的方法
2.synchornized修飾的是代碼塊
當(dāng)synchornized修飾的是代碼塊的時候,synchornized關(guān)鍵字會被編譯成monitorenter和monitorexit,使得同一時刻只能有一個線程進(jìn)入到同步代碼塊中,但是這里為什么會有二個monitorexit,是因?yàn)槌绦蛘M顺龅臅r候需要釋放鎖,在程序異常的時候也要釋放鎖,所以會對應(yīng)二個
無論synchornized修飾的是方法還是代碼塊,底層都是通過JVM調(diào)用操作系統(tǒng)的Mutes鎖實(shí)現(xiàn)的,當(dāng)線程被阻塞時會被掛起,等待CPU重新調(diào)度,這會導(dǎo)致線程在操作系統(tǒng)的用戶態(tài)和內(nèi)核態(tài)之間切換,影響性能
Monitor鎖原理
synchornized低成是基于Monitor鎖來實(shí)現(xiàn)的,而Monitor鎖是基于操作系統(tǒng)的Mutex鎖實(shí)現(xiàn)的,Mutex鎖是操作系統(tǒng)級別的重量級鎖,所以性能較低
在Java中,創(chuàng)建的任何一個對象在JVM中都會關(guān)聯(lián)一個Monitor對象,所以說任何一個對象都可以成為鎖。
在HotSpot JVM中,Monitor是由ObjectMoitor實(shí)現(xiàn)的,在ObjectMonitor對象的數(shù)據(jù)結(jié)構(gòu)中,有幾個重要的屬性
- _WaitSet:是一個集合,當(dāng)線程獲到鎖之后,但是還沒有完成業(yè)務(wù)邏輯,也還沒釋放鎖,這時候調(diào)用了Object類的wait()方法,這時候這個線程就會進(jìn)入_WaitSet這個集合中等待被喚醒,也就是執(zhí)行nitify()或者notifyAll()方法喚醒
- _EntryList:是一個集合,當(dāng)有多個線程來獲取鎖,這時候只有一個線程能成功拿到鎖,剩下那些沒有拿到鎖的線程就會進(jìn)入_EntryList集合中,等待下次搶鎖
- _Owner:當(dāng)一個線程獲取到鎖之后,就會將該值設(shè)置成當(dāng)前線程,釋放鎖之后,這個值就會重新被設(shè)置成null
- _count:當(dāng)一個線程獲取到鎖之后,_count的值就會+1,釋放鎖之后就會-1,只有當(dāng)減到0之后,才算真正的釋放掉鎖了,其它線程才能來獲取這把鎖,synchornized可重入鎖也是基于這個值來實(shí)現(xiàn)的
所以當(dāng)多個線程同時訪問被synchornized修飾的方法或者代碼塊時候,synchornized加鎖和釋放鎖的底層實(shí)現(xiàn)流程大致為:
- 1:進(jìn)入_EntryList集合,當(dāng)某個線程獲取到鎖之后,這個線程就會進(jìn)入_Owner區(qū)域,就會將Monitor對象的_owner變量復(fù)制為當(dāng)前線程。并把_count值+1
- 2:當(dāng)線程調(diào)用wait()方法時,當(dāng)前線程會釋放掉持有的Monitor對象,并把_owner賦值成null,_count的值-1,同時這個線程就會進(jìn)入_WaitSet集合等到被喚醒
- 3:如果獲取到鎖的線程執(zhí)行完畢,也會釋放Monitor鎖。,_owner被置為null,_count被置為0
偏向鎖
雖然在程序的方法中或代碼塊中添加了synchornized,但是在大部分的情況下,不會存在多線程競爭這種情況,并且會出現(xiàn)同一個線程多次獲取同一把鎖的現(xiàn)象,為了提升這種情況下程序的性能,引入了偏向鎖
輕量級鎖
當(dāng)多線程競爭鎖不激烈時,可以通過CAS機(jī)制競爭鎖,這就是輕量級鎖,引入輕量級鎖的目的是在多線程競爭鎖不激烈時,避免由于使用操作系統(tǒng)層面的Mutex重量級鎖導(dǎo)致性能低下
重量級鎖
重量級鎖主要是基于操作系統(tǒng)的Mutex鎖實(shí)現(xiàn),重量級鎖的執(zhí)行效率較低,處于重量級鎖時被阻塞的線程不會消耗CPU資源
鎖升級過程
多個線程在爭搶synchornized鎖時,在某些情況下,會由無鎖狀態(tài)一步步升級為最終的重量級鎖,整個升級過程大致包括如下幾個步驟
- 1:線程在競爭synchornized時,JVM首先會檢查鎖對象的Mark Word中偏向鎖的標(biāo)記位是否為1,鎖標(biāo)記位是否為01,如果二個條件都滿足,則當(dāng)前鎖處于偏向鎖狀態(tài)
- 2:爭搶synchornized鎖線程檢查鎖對象的Mark Work中存儲的線程ID是否是自己的,如果是自己的線程ID,則表示處于偏向鎖狀態(tài),當(dāng)前線程可以直接進(jìn)入方法或者代碼塊
- 3:如果鎖對象的Mark Word的線程ID不是自己的線程ID,那么就會通過CAS方式來競爭鎖資源,如果獲取到鎖資源了,就將Mark Word中存儲的線程ID修改成自己的線程ID,將偏向鎖的標(biāo)記設(shè)置成1,鎖標(biāo)記位置設(shè)置成01,當(dāng)前鎖處于偏向鎖狀態(tài)
- 4:如果當(dāng)前線程通過CAS沒有獲取到鎖資源,則說明有其它線程也在爭搶資源,此時會撤銷偏向鎖,升級為輕量級鎖,并將Mark Word的鎖標(biāo)記為都清空
- 5:當(dāng)前線程與其它線程還是會通過CAS方式來競爭資源,如果某個線程成功獲取到資源,就會將鎖對象的Mark Word中的鎖標(biāo)志位設(shè)置成00,此時進(jìn)入輕量級鎖狀態(tài)
- 6:競爭失敗的線程還是會通過CAS方式來獲取鎖,但是當(dāng)CAS達(dá)到一定的次數(shù)以后,就會升級為重量級鎖了
以上就是深入講解Java synchronized的核心原理的詳細(xì)內(nèi)容,更多關(guān)于Java synchronized的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
maven父子工程多模塊統(tǒng)一管理版本號的解決方法
maven父子工程多模塊,每個模塊還都可以獨(dú)立存在,子模塊往往通常希望和父工程保持一樣的版本,如果每個工程單獨(dú)定義版本號,后期變更打包也非常麻煩,,所以本文給大家介紹了maven父子工程多模塊如何管理統(tǒng)一的版本號,需要的朋友可以參考下2024-09-09Java concurrency線程池之線程池原理(一)_動力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要為大家詳細(xì)介紹了Java concurrency線程池之線程池原理,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06Spring?Cloud?Sleuth?和?Zipkin?進(jìn)行分布式跟蹤使用小結(jié)
分布式跟蹤是一種機(jī)制,我們可以使用它跟蹤整個分布式系統(tǒng)中的特定請求,分布式跟蹤允許您跟蹤分布式系統(tǒng)中的請求,本文給大家介紹Spring?Cloud?Sleuth?和?Zipkin?進(jìn)行分布式跟蹤使用小結(jié),感興趣的朋友一起看看吧2022-03-03java中獲取當(dāng)前服務(wù)器的Ip地址的方法
本篇文章主要介紹了java中獲取當(dāng)前服務(wù)器的Ip地址的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-02-02