Java?synchronized同步關(guān)鍵字工作原理
一、簡(jiǎn)介
synchronized是一個(gè)同步關(guān)鍵字,在某些多線程場(chǎng)景下,如果不進(jìn)行同步會(huì)導(dǎo)致共享數(shù)據(jù)不安全,synchronized關(guān)鍵字就可以用于代碼同步。
synchronized主要有3種使用形式:
- 修飾普通同步方法
鎖的對(duì)象是當(dāng)前實(shí)例對(duì)象;
- 修飾靜態(tài)同步方法
鎖的對(duì)象是當(dāng)前的類的Class字節(jié)碼對(duì)象;
- 修飾同步代碼塊
鎖的對(duì)象是synchronized后面括號(hào)里配置的對(duì)象,可以是某個(gè)對(duì)象,也可以是某個(gè)類的.class對(duì)象;
二、synchronized的特性
1)、原子性
原子性指的是在一次或多次操作中,要么所有的操作都執(zhí)行并且不會(huì)受其他因素干擾而中斷,要么所有的操作都不執(zhí)行。
2)、可見性
可見性是指一個(gè)線程對(duì)共享變量進(jìn)行了修改,另一個(gè)線程可以立即讀取得到修改后的最新值。
獲取鎖時(shí),會(huì)清空當(dāng)前線程工作內(nèi)存中共享變量的副本值,重新從主內(nèi)存中獲取變量最新的值;
釋放鎖時(shí),會(huì)將工作內(nèi)存的值重新刷新回主內(nèi)存;
3)、有序性
有序性是指程序中代碼的執(zhí)行順序,Java在編譯時(shí)和運(yùn)行時(shí)會(huì)對(duì)代碼進(jìn)行優(yōu)化,會(huì)導(dǎo)致程序最終的執(zhí)行順序不一定就是我們編寫代碼時(shí)的順序。
例如,instance = new Singleton()實(shí)例化對(duì)象的語(yǔ)句分為三步:
- 1、分配對(duì)象的內(nèi)存空間;
- 2、初始化對(duì)象;
- 3、設(shè)置實(shí)例對(duì)象指向剛分配的內(nèi)存地址;
上述第二步操作需要依賴第一步,但是第三步操作不需要依賴第二步,所以執(zhí)行順序可能為:1->2->3、1->3->2,當(dāng)執(zhí)行順序?yàn)?->3->2時(shí),可能實(shí)例對(duì)象還沒正確初始化,我們直接拿到使用的時(shí)候可能會(huì)報(bào)錯(cuò)。
synchronized的有序性是依靠?jī)?nèi)存屏障實(shí)現(xiàn)的,在 monitorenter 指令和 Load 屏障之后,會(huì)加一個(gè) Acquire屏障,這個(gè)屏障的作用是禁止同步代碼塊里面的讀操作和外面的讀寫操作之間發(fā)生指令重排,在 monitorexit 指令前加一個(gè)Release屏障,也是禁止同步代碼塊里面的寫操作和外面的讀寫操作之間發(fā)生重排序。如下:
int a = 0; synchronize (this){ //monitorenter // Load內(nèi)存屏障 // Acquire屏障,禁止代碼塊內(nèi)部的讀,和外面的讀寫發(fā)生指令重排 int b = a; a = 10; //注意:內(nèi)部還是會(huì)發(fā)生指令重排 // Release屏障,禁止寫,和外面的讀寫發(fā)生指令重排 } //monitorexit //Store內(nèi)存屏障
4)、可重入特性
可重入指的就是一個(gè)線程可以多次執(zhí)行synchronized,重復(fù)獲取同一把鎖。
舉個(gè)例子:
public class RenentrantDemo { // 鎖對(duì)象 private static Object obj = new Object(); public static void main(String[] args) { // 自定義Runnable對(duì)象 Runnable runnable = () -> { // 使用嵌套的同步代碼塊 synchronized (obj) { System.out.println(Thread.currentThread().getName() + "第一次獲取鎖資源..."); synchronized (obj) { System.out.println(Thread.currentThread().getName() + "第二次獲取鎖資源..."); synchronized (obj) { System.out.println(Thread.currentThread().getName() + "第三次獲取鎖資源..."); } } } }; new Thread(runnable, "t1").start(); } }
運(yùn)行結(jié)果:
t1第一次獲取鎖資源...
t1第二次獲取鎖資源...
t1第三次獲取鎖資源...
三、synchonized的使用及通過(guò)反匯編分析其原理
修飾代碼塊
public class SynchronizedDemo01 { // 鎖對(duì)象 private static Object obj = new Object(); public static void main(String[] args) { synchronized (obj) { System.out.println("execute main()..."); } } }
使用javap -p -v .\SynchronizedDemo01.class命令對(duì)字節(jié)碼進(jìn)行反匯編,查看字節(jié)碼指令:
monitorenter指令
官網(wǎng)對(duì)monitorenter指令的介紹,就是說(shuō)每一個(gè)對(duì)象都會(huì)和一個(gè)監(jiān)視器對(duì)象monitor關(guān)聯(lián),監(jiān)視器被占用時(shí)會(huì)被鎖住,其他線程無(wú)法來(lái)獲取該monitor。 當(dāng)JVM執(zhí)行某個(gè)線程的某個(gè)方法內(nèi)部的monitorenter時(shí),它會(huì)嘗試去獲取當(dāng)前對(duì)象對(duì)應(yīng)的monitor的所有權(quán)。大體過(guò)程如下:
1. 若monior的進(jìn)入數(shù)為0,線程可以進(jìn)入monitor,并將monitor的進(jìn)入數(shù)置為1,當(dāng)前線程成為monitor的owner(擁有這把鎖的線程);
2. 若線程已擁有monitor的所有權(quán),允許它重入monitor,則進(jìn)入monitor的進(jìn)入數(shù)加1(記錄線程擁有鎖的次數(shù));
3. 若其他線程已經(jīng)占有monitor的所有權(quán),那么當(dāng)前嘗試獲取monitor的所有權(quán)的線程會(huì)被阻塞,直到monitor的進(jìn)入數(shù)變?yōu)?,才能重新嘗試獲取monitor的所有權(quán);
monitorexit指令
官網(wǎng)對(duì)monitorexit指令的介紹,就是說(shuō)能執(zhí)行monitorexit指令的線程一定是擁有當(dāng)前對(duì)象的monitor的所有權(quán)的線程;執(zhí)行monitorexit時(shí)會(huì)將monitor的進(jìn)入數(shù)減1,當(dāng)monitor的進(jìn)入數(shù)減為0時(shí),當(dāng)前線程退出。
為什么字節(jié)碼中存在兩個(gè)monitorexit指令?
其實(shí)第二個(gè)monitorexit指令,是在程序發(fā)生異常時(shí)候用到的,也就說(shuō)明了synchronized在發(fā)生異常時(shí),會(huì)自動(dòng)釋放鎖。
ObjectMonitor對(duì)象監(jiān)視器結(jié)構(gòu)如下:
ObjectMonitor() { _header = NULL; //鎖對(duì)象的原始對(duì)象頭 _count = 0; //搶占當(dāng)前鎖的線程數(shù)量 _waiters = 0, //調(diào)用wait方法后等待的線程數(shù)量 _recursions = 0; //記錄鎖重入次數(shù) _object = NULL; _owner = NULL; //指向持有ObjectMonitor的線程 _WaitSet = NULL; //處于wait狀態(tài)的線程隊(duì)列,等待被喚醒 _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; //等待鎖的線程隊(duì)列 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; _previous_owner_tid = 0; }
修飾普通方法
public class SynchronizedDemo02 { public static void main(String[] args) { } // 修飾普通方法 public synchronized void add() { System.out.println("add..."); } }
使用javap -p -v .\SynchronizedDemo02.class命令對(duì)字節(jié)碼進(jìn)行反匯編,查看字節(jié)碼指令:
如上圖,我們可以看到同步方法在反匯編后,不再是通過(guò)插入monitorentry和monitorexit指令實(shí)現(xiàn),而是會(huì)增加 ACC_SYNCHRONIZED 標(biāo)識(shí)隱式實(shí)現(xiàn)的,如果方法表結(jié)構(gòu)(method_info Structure)中的ACC_SYNCHRONIZED標(biāo)志被設(shè)置,那么線程在執(zhí)行方法前會(huì)先去獲取對(duì)象的monitor對(duì)象,如果獲取成功則執(zhí)行方法代碼,執(zhí)行完畢后釋放monitor對(duì)象,如果monitor對(duì)象已經(jīng)被其它線程獲取,那么當(dāng)前線程被阻塞。
修飾靜態(tài)方法
public class SynchronizedDemo03 { public static void main(String[] args) { add(); } // 修飾靜態(tài)方法 public synchronized static void add() { System.out.println("add..."); } }
使用javap -p -v .\SynchronizedDemo03.class命令對(duì)字節(jié)碼進(jìn)行反匯編,查看字節(jié)碼指令:
四、synchronized鎖對(duì)象存在哪里
之前對(duì)對(duì)象的內(nèi)存布局的介紹中,我們知道一個(gè)對(duì)象,包括對(duì)象頭、實(shí)例數(shù)據(jù)、對(duì)齊填充。而對(duì)象頭又包括mark word標(biāo)記字、類型指針、數(shù)組長(zhǎng)度(只有數(shù)組對(duì)象才有)。在mark word標(biāo)記字中,有一塊區(qū)域主要存放關(guān)于鎖的信息。
存在鎖對(duì)象的對(duì)象頭的MarkWord標(biāo)記字中。如下圖:
五、synchronized與lock的區(qū)別
區(qū)別 | synchronized | lock |
1 | 關(guān)鍵字 | 接口 |
2 | 自動(dòng)釋放鎖 | 必須手動(dòng)調(diào)用unlock()方法釋放鎖 |
3 | 不能知道線程是否拿到鎖 | 可以知道線程是否拿到鎖 |
4 | 能鎖住方法和代碼塊 | 只能鎖住代碼塊 |
5 | 讀、寫操作都阻塞 | 可以使用讀鎖,提高多線程讀效率; |
6 | 非公平鎖 | 通過(guò)構(gòu)造方法可指定是公平鎖/非公平鎖 |
六、總結(jié)
1、synchronized修飾代碼塊的時(shí)候,通過(guò)在生成的字節(jié)碼指令中插入monitorenter和monitorexit指令來(lái)完成對(duì)對(duì)象監(jiān)視器鎖的獲取和釋放;
2、synchronized修飾普通方法和靜態(tài)方法的時(shí)候,通過(guò)在字節(jié)碼中的方法頭信息中添ACC_SYNCHRONIZED標(biāo)識(shí),線程在執(zhí)行方法前會(huì)先去獲取對(duì)象的monitor對(duì)象,如果獲取成功則執(zhí)行方法代碼,執(zhí)行完畢后釋放monitor對(duì)象;
3、synchronized修飾代碼塊,鎖的對(duì)象就是代碼塊中的對(duì)象;修飾普通方法的時(shí)候,鎖的對(duì)象就是當(dāng)前對(duì)象this;修飾靜態(tài)方法的時(shí)候,鎖的對(duì)象就是當(dāng)前類的Class字節(jié)碼對(duì)象(類對(duì)象);
4、使用synchronized修飾實(shí)例對(duì)象時(shí),如果一個(gè)線程正在訪問實(shí)例對(duì)象的一個(gè)synchronized方法時(shí),其它線程不僅不能訪問該synchronized方法,該對(duì)象的其它synchronized方法也不能訪問,因?yàn)橐粋€(gè)對(duì)象只有一個(gè)監(jiān)視器鎖對(duì)象,但是其它線程可以訪問該對(duì)象的非synchronized方法。
5、線程A訪問實(shí)例對(duì)象的非static synchronized方法時(shí),線程B也可以同時(shí)訪問實(shí)例對(duì)象的static synchronized方法,因?yàn)榍罢攉@取的是實(shí)例對(duì)象的監(jiān)視器鎖,而后者獲取的是類對(duì)象的監(jiān)視器鎖,兩者不存在互斥關(guān)系。
到此這篇關(guān)于Java synchronized同步關(guān)鍵字工作原理的文章就介紹到這了,更多相關(guān)Java synchronized內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java同步鎖Synchronized底層源碼和原理剖析(推薦)
- java同步鎖的正確使用方法(必看篇)
- 95%的Java程序員人都用不好Synchronized詳解
- Java synchronized偏向鎖的概念與使用
- Java?synchronized輕量級(jí)鎖實(shí)現(xiàn)過(guò)程淺析
- Java synchronized重量級(jí)鎖實(shí)現(xiàn)過(guò)程淺析
- Java @Transactional與synchronized使用的問題
- Java?synchronized與死鎖深入探究
- Java synchronized與CAS使用方式詳解
- 淺析Java關(guān)鍵詞synchronized的使用
- synchronized及JUC顯式locks?使用原理解析
- java鎖synchronized面試常問總結(jié)
- Java?HashTable與Collections.synchronizedMap源碼深入解析
- Java?Synchronized鎖的使用詳解
- AQS加鎖機(jī)制Synchronized相似點(diǎn)詳解
- Java必會(huì)的Synchronized底層原理剖析
- 一個(gè)例子帶你看懂Java中synchronized關(guān)鍵字到底怎么用
- 詳解Java?Synchronized的實(shí)現(xiàn)原理
- Synchronized?和?ReentrantLock?的實(shí)現(xiàn)原理及區(qū)別
- Java同步鎖synchronized用法的最全總結(jié)
相關(guān)文章
MyBatis?Plus如何實(shí)現(xiàn)獲取自動(dòng)生成主鍵值
這篇文章主要介紹了MyBatis?Plus如何實(shí)現(xiàn)獲取自動(dòng)生成主鍵值問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-09-09Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(22)
下面小編就為大家?guī)?lái)一篇Java基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧,希望可以幫到你2021-07-07java實(shí)現(xiàn)爬蟲爬網(wǎng)站圖片的實(shí)例代碼
這篇文章主要介紹了java實(shí)現(xiàn)爬蟲爬網(wǎng)站圖片的實(shí)例代碼,需要的朋友可以參考下2018-06-06解決IDEA service層跳轉(zhuǎn)實(shí)現(xiàn)類的快捷圖標(biāo)消失問題
這篇文章主要介紹了解決IDEA service層跳轉(zhuǎn)實(shí)現(xiàn)類的快捷圖標(biāo)消失問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02spring batch使用reader讀數(shù)據(jù)的內(nèi)存容量問題詳解
這篇文章主要介紹了spring batch使用reader讀數(shù)據(jù)的內(nèi)存容量問題詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07