Java多線程并發(fā)synchronized?關(guān)鍵字
基礎(chǔ)
Java 在虛擬機(jī)層面提供了 synchronized
關(guān)鍵字供開(kāi)發(fā)者快速實(shí)現(xiàn)互斥同步的重量級(jí)鎖來(lái)保障線程安全。
synchronized 關(guān)鍵字可用于兩種場(chǎng)景:
- 修飾方法。
- 持有一個(gè)對(duì)象,并執(zhí)行一個(gè)代碼塊。
而根據(jù)加鎖的對(duì)象不同,又分為兩種情況:
- 對(duì)象鎖
- 類對(duì)象鎖
以下代碼示例是 synchronized
的具體用法:
1. 修飾方法 synchronized void function() { ... } 2. 修飾靜態(tài)方法 static synchronized void function() { ... } 3. 對(duì)對(duì)象加鎖 synchronized(object) { // ... }
修飾普通方法
synchronized
修飾方法加鎖,相當(dāng)于對(duì)當(dāng)前對(duì)象加鎖,類 A 中的 function()
是一個(gè) synchronized
修飾的普通方法:
class A { synchronized void function() { ... } }
它等效于:
class A { void function() { ? synchronized(this) { ... } ? } }
結(jié)論:synchronized
修飾普通方法,實(shí)際上是對(duì)當(dāng)前對(duì)象進(jìn)行加鎖處理,也就是對(duì)象鎖。
修飾靜態(tài)方法
synchronized
修飾靜態(tài)方法,相當(dāng)于對(duì)靜態(tài)方法所屬類的 class 對(duì)象進(jìn)行加鎖,這里的 class 對(duì)象是 JVM 在進(jìn)行類加載時(shí)創(chuàng)建的代表當(dāng)前類的 java.lang.Class
對(duì)象,每個(gè)類都有唯一的 Class 對(duì)象。這種對(duì) Class 對(duì)象加鎖,稱之為類對(duì)象鎖。
類加載階段主要做了三件事情:
根據(jù)特定名稱查找類或接口類型的二進(jìn)制字節(jié)流。
將這個(gè)二進(jìn)制字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
在內(nèi)存中生成一個(gè)代表這個(gè)類的 java.lang.Class 對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問(wèn)入口。
class A { static synchronized void function() { ... } // 相當(dāng)于對(duì) class 對(duì)象加鎖,這里只是描述,靜態(tài)方法和普通方法不可等效。 void function() { ? ? ? ?synchronized(A.class) { ... } } }
也就是說(shuō),如果一個(gè)普通方法中持有了 A.class
,那么就會(huì)與靜態(tài)方法 function()
互斥,因?yàn)楸举|(zhì)上它們加鎖的對(duì)象是同一個(gè)。
Synchronized 加鎖原理
public class Sync { ? ?Object lock = new Object(); ? ?public void function() { ? ? ? ?synchronized (lock) { ? ? ? ? ? ?System.out.print("lock"); ? ? ? } ? } }
這是一個(gè)簡(jiǎn)單的 synchronized
關(guān)鍵字對(duì) lock 對(duì)象進(jìn)行加鎖的 demo ,經(jīng)過(guò)javac Sync.java
命令反編譯生成 class 文件,然后通過(guò) javap -verbose Sync
命令查看內(nèi)容:
public void function(); ? descriptor: ()V ? flags: (0x0001) ACC_PUBLIC ? Code: ? ? stack=2, locals=3, args_size=1 ? ? ? ? 0: aload_0 ? ? ? ? 1: getfield ? ? #7 ? ? ? ? ? ? ? ? // Field lock:Ljava/lang/Object; ? ? ? ? 4: dup ? ? ? ? 5: astore_1 ? ? ? ? 6: monitorenter // 【1】 ? ? ? ? 7: getstatic ? ? #13 ? ? ? ? ? ? ? ? // Field java/lang/System.out:Ljava/io/PrintStream; ? ? ? 10: ldc ? ? ? ? ? #19 ? ? ? ? ? ? ? ? // String lock ? ? ? 12: invokevirtual #20 ? ? ? ? ? ? ? ? // Method java/io/PrintStream.print:(Ljava/lang/String;)V ? ? ? 15: aload_1 ? ? ? 16: monitorexit // 【2】 ? ? ? 17: goto ? ? ? ? 25 ? ? ? 20: astore_2 ? ? ? 21: aload_1 ? ? ? 22: monitorexit // 【3】 ? ? ? 23: aload_2 ? ? ? 24: athrow ? ? ? 25: return
【1】與【2】處的 monitorenter
和 monitorexit
兩個(gè)指令就是加鎖操作的關(guān)鍵。
而【3】處的 monitorexit
,是為了保證在同步代碼塊中出現(xiàn) Exception 或者 Error 時(shí),通過(guò)調(diào)用第二個(gè)monitorexit
指令來(lái)保證釋放鎖。
monitorenter
指令會(huì)讓對(duì)象在對(duì)象頭中的鎖計(jì)數(shù)器計(jì)數(shù) + 1, monitorexit
指令則相反,計(jì)數(shù)器 - 1。
monitor 鎖的底層邏輯
對(duì)象會(huì)關(guān)聯(lián)一個(gè) monitor ,
monitorenter
指令會(huì)檢查對(duì)象是否管理了 monitor 如果沒(méi)有創(chuàng)建一個(gè) ,并將其關(guān)聯(lián)到這個(gè)對(duì)象。monitor 內(nèi)部有兩個(gè)重要的成員變量 owner(擁有這把鎖的線程)和 recursions(記錄線程擁有鎖的次數(shù)),當(dāng)一個(gè)線程擁有 monitor 后其他線程只能等待。
加鎖意味著在同一時(shí)間內(nèi),對(duì)象只能被一個(gè)線程獲取到。
monitorenter
monitorenter
指令標(biāo)記了同步代碼塊的開(kāi)始位置,也就是這個(gè)時(shí)候會(huì)創(chuàng)建一個(gè) monitor ,然后當(dāng)前線程會(huì)嘗試獲取這個(gè) monitor 。
monitorenter
指令觸發(fā)時(shí),線程嘗試獲取 monitor 鎖有三種邏輯:
- monitor 鎖計(jì)數(shù)器為 0 ,意味著目前還沒(méi)有被任意線程持有,那這個(gè)線程就會(huì)立刻持有這個(gè) monitor 鎖,然后把鎖計(jì)數(shù)器+1,一旦+1,別的線程再想獲取,就需要等待。
- 如果又對(duì)當(dāng)前對(duì)象執(zhí)行了一個(gè)
monitorenter
指令,那么對(duì)象關(guān)聯(lián)的 monitor 已經(jīng)存在,就會(huì)把鎖計(jì)數(shù)器 + 1,鎖計(jì)數(shù)器的值此時(shí)是 2,并且隨著重入的次數(shù),會(huì)一直累加。 - monitor 鎖已被其他線程持有,鎖計(jì)數(shù)器不為 0 ,當(dāng)前線程等待鎖釋放。
monitorexit
monitorexit
指令會(huì)對(duì)鎖計(jì)數(shù)器進(jìn)行 - 1 ,如果在執(zhí)行 - 1 后鎖計(jì)數(shù)器仍不為 0 ,持有鎖的線程仍持有這個(gè)鎖,直到鎖計(jì)數(shù)器等于 0 ,持有線程才釋放了鎖。
任意線程訪問(wèn)加鎖對(duì)象時(shí),首先要獲取對(duì)象的 monitor ,如果獲取失敗,該現(xiàn)場(chǎng)進(jìn)入阻塞狀態(tài),即 Blocked。當(dāng)這個(gè)對(duì)象的 monitor 被持有線程釋放后,阻塞等待的線程就有機(jī)會(huì)獲取到這個(gè) monitor 。
synchronized 修飾靜態(tài)方法
根據(jù)鎖計(jì)數(shù)器的原理,理論上說(shuō), monitorenter
和 monitorexit
兩個(gè)指令應(yīng)該成對(duì)出現(xiàn)(拋除處理 Exception 或 Error 的 monitorexit
)。重復(fù)對(duì)同一個(gè)線程進(jìn)行加鎖。
我們來(lái)寫(xiě)一個(gè)示例檢查一下:
public class Sync { ? ?Object lock = new Object(); ? ?public void function() { ? ? ? ?synchronized (Sync.class) { ? ? ? ? ? ?System.out.print("lock"); ? ? ? ? ? ?method(); ? ? ? } ? } ? ?synchronized static void method() { ? ? ? ?System.out.print("method"); ? }; }
synchronized (Sync.class)
先持有了 Sync 的類對(duì)象,然后再通過(guò) synchronized
靜態(tài)方法進(jìn)行一次加鎖,理論上說(shuō),反編譯后應(yīng)該是出現(xiàn)兩對(duì) monitorenter
和 monitorexit
,查看反編譯 class 文件:
public void function(); ? descriptor: ()V ? flags: (0x0001) ACC_PUBLIC ? Code: ? ? stack=2, locals=3, args_size=1 ? ? ? ? 0: ldc ? ? ? ? ? #8 ? ? ? ? ? ? ? ? // class javatest/Sync ? ? ? ? 2: dup ? ? ? ? 3: astore_1 ? ? ? ? 4: monitorenter ? ? ? ? 5: getstatic ? ? #13 ? ? ? ? ? ? ? ? // Field java/lang/System.out:Ljava/io/PrintStream; ? ? ? ? 8: ldc ? ? ? ? ? #19 ? ? ? ? ? ? ? ? // String lock ? ? ? 10: invokevirtual #20 ? ? ? ? ? ? ? ? // Method java/io/PrintStream.print:(Ljava/lang/String;)V ? ? ? 13: invokestatic #26 ? ? ? ? ? ? ? ? // Method method:()V ? ? ? 16: aload_1 ? ? ? 17: monitorexit ? ? ? 18: goto ? ? ? ? 26 ? ? ? 21: astore_2 ? ? ? 22: aload_1 ? ? ? 23: monitorexit ? ? ? 24: aload_2 ? ? ? 25: athrow ? ? ? 26: return
method
方法的字節(jié)碼:
static synchronized void method(); ? descriptor: ()V ? flags: (0x0028) ACC_STATIC, ACC_SYNCHRONIZED ? Code: ? ? stack=2, locals=0, args_size=0 ? ? ? ? 0: getstatic ? ? #13 ? ? ? ? ? ? ? ? // Field java/lang/System.out:Ljava/io/PrintStream; ? ? ? ? 3: ldc ? ? ? ? ? #29 ? ? ? ? ? ? ? ? // String method ? ? ? ? 5: invokevirtual #20 ? ? ? ? ? ? ? ? // Method java/io/PrintStream.print:(Ljava/lang/String;)V ? ? ? ? 8: return
神奇的現(xiàn)象出現(xiàn)了,monitorenter
出現(xiàn)了一次, monitorexit
出現(xiàn)了兩次,這和我們最開(kāi)始只加一次鎖的 demo 一致了。
那么是不是因?yàn)殪o態(tài)方法的原因呢,我們將 demo 改造成下面的效果:
public class Sync { ? ?public void function() { ? ? ? ?synchronized (Sync.class) { ? ? ? ? ? ?System.out.print("lock"); ? ? ? } ? ? ? ?method(); ? } ? ?void method() { ? ? ? ?synchronized (Sync.class) { ? ? ? ? ? ?System.out.print("method"); ? ? ? } ? } }
反編譯結(jié)果:
public void function(); ? descriptor: ()V ? flags: (0x0001) ACC_PUBLIC ? Code: ? ? stack=2, locals=3, args_size=1 ? ? ? ? 0: ldc ? ? ? ? ? #7 ? ? ? ? ? ? ? ? // class javatest/Sync ? ? ? ? 2: dup ? ? ? ? 3: astore_1 ? ? ? ? 4: monitorenter ? ? ? ? 5: getstatic ? ? #9 ? ? ? ? ? ? ? ? // Field java/lang/System.out:Ljava/io/PrintStream; ? ? ? ? 8: ldc ? ? ? ? ? #15 ? ? ? ? ? ? ? ? // String lock ? ? ? 10: invokevirtual #17 ? ? ? ? ? ? ? ? // Method java/io/PrintStream.print:(Ljava/lang/String;)V ? ? ? 13: aload_0 ? ? ? 14: invokevirtual #23 ? ? ? ? ? ? ? ? // Method method:()V ? ? ? 17: aload_1 ? ? ? 18: monitorexit ? ? ? 19: goto ? ? ? ? 27 ? ? ? 22: astore_2 ? ? ? 23: aload_1 ? ? ? 24: monitorexit ? ? ? 25: aload_2 ? ? ? 26: athrow ? ? ? 27: return
method
方法的編譯結(jié)果:
void method(); ? descriptor: ()V ? flags: (0x0000) ? Code: ? ? stack=2, locals=3, args_size=1 ? ? ? ? 0: ldc ? ? ? ? ? #7 ? ? ? ? ? ? ? ? // class javatest/Sync ? ? ? ? 2: dup ? ? ? ? 3: astore_1 ? ? ? ? 4: monitorenter ? ? ? ? 5: getstatic ? ? #9 ? ? ? ? ? ? ? ? // Field java/lang/System.out:Ljava/io/PrintStream; ? ? ? ? 8: ldc ? ? ? ? ? #26 ? ? ? ? ? ? ? ? // String method ? ? ? 10: invokevirtual #17 ? ? ? ? ? ? ? ? // Method java/io/PrintStream.print:(Ljava/lang/String;)V ? ? ? 13: aload_1 ? ? ? 14: monitorexit ? ? ? 15: goto ? ? ? ? 23 ? ? ? 18: astore_2 ? ? ? 19: aload_1 ? ? ? 20: monitorexit ? ? ? 21: aload_2 ? ? ? 22: athrow ? ? ? 23: return
從這里看,的確是出現(xiàn)了兩組 monitorenter
和monitorexit
。
而從靜態(tài)方法的 flags: (0x0028) ACC_STATIC, ACC_SYNCHRONIZED
中,我們可以看出,JVM 對(duì)于同步靜態(tài)方法并不是通過(guò)monitorenter
和 monitorexit
實(shí)現(xiàn)的,而是通過(guò)方法的 flags 中添加 ACC_SYNCHRONIZED
標(biāo)記實(shí)現(xiàn)的。
而如果換一種方式,不使用嵌套加鎖,改為連續(xù)執(zhí)行兩次對(duì)同一個(gè)對(duì)象加鎖解鎖:
public void function() { ? synchronized (Sync.class) { ? ? ? System.out.print("lock"); ? } ? method(); }
反編譯:
public void function(); ? descriptor: ()V ? flags: (0x0001) ACC_PUBLIC ? Code: ? ? stack=2, locals=3, args_size=1 ? ? ? ? 0: ldc ? ? ? ? ? #7 ? ? ? ? ? ? ? ? // class javatest/Sync ? ? ? ? 2: dup ? ? ? ? 3: astore_1 ? ? ? ? 4: monitorenter ? ? ? ? 5: getstatic ? ? #9 ? ? ? ? ? ? ? ? // Field java/lang/System.out:Ljava/io/PrintStream; ? ? ? ? 8: ldc ? ? ? ? ? #15 ? ? ? ? ? ? ? ? // String lock ? ? ? 10: invokevirtual #17 ? ? ? ? ? ? ? ? // Method java/io/PrintStream.print:(Ljava/lang/String;)V ? ? ? 13: aload_1 ? ? ? 14: monitorexit ? ? ? 15: goto ? ? ? ? 23 ? ? ? 18: astore_2 ? ? ? 19: aload_1 ? ? ? 20: monitorexit ? ? ? 21: aload_2 ? ? ? 22: athrow ? ? ? 23: aload_0 ? ? ? 24: invokevirtual #23 ? ? ? ? ? ? ? ? // Method method:()V ? ? ? 27: return
method
方法的編譯結(jié)果是:
void method(); ? descriptor: ()V ? flags: (0x0000) ? Code: ? ? stack=2, locals=3, args_size=1 ? ? ? ? 0: ldc ? ? ? ? ? #7 ? ? ? ? ? ? ? ? // class javatest/Sync ? ? ? ? 2: dup ? ? ? ? 3: astore_1 ? ? ? ? 4: monitorenter ? ? ? ? 5: getstatic ? ? #9 ? ? ? ? ? ? ? ? // Field java/lang/System.out:Ljava/io/PrintStream; ? ? ? ? 8: ldc ? ? ? ? ? #26 ? ? ? ? ? ? ? ? // String method ? ? ? 10: invokevirtual #17 ? ? ? ? ? ? ? ? // Method java/io/PrintStream.print:(Ljava/lang/String;)V ? ? ? 13: aload_1 ? ? ? 14: monitorexit ? ? ? 15: goto ? ? ? ? 23 ? ? ? 18: astore_2 ? ? ? 19: aload_1 ? ? ? 20: monitorexit ? ? ? 21: aload_2 ? ? ? 22: athrow ? ? ? 23: return
看來(lái)結(jié)果也是一樣的,monitorenter
和 monitorexit
成對(duì)出現(xiàn)。
優(yōu)點(diǎn)、缺點(diǎn)及優(yōu)化
synchronized
關(guān)鍵字是 JVM 提供的 API ,是重量級(jí)鎖,所以它具有重量級(jí)鎖的優(yōu)點(diǎn),保持嚴(yán)格的互斥同步。
而缺點(diǎn)則同樣是互斥同步的角度來(lái)說(shuō)的:
- 效率低:鎖的釋放情況少,只有代碼執(zhí)行完畢或者異常結(jié)束才會(huì)釋放鎖;試圖獲取鎖的時(shí)候不能設(shè)定超時(shí),不能中斷一個(gè)正在使用鎖的線程,相對(duì)而言,
Lock
可以中斷和設(shè)置超時(shí)。 - 不夠靈活:加鎖和釋放的時(shí)機(jī)單一,每個(gè)鎖僅有一個(gè)單一的條件(某個(gè)對(duì)象)。
優(yōu)化方案:Java 提供了java.util.concurrent
包,其中 Lock
相關(guān)的一些 API ,拓展了很多功能,可以考慮使用 J.U.C 中豐富的鎖機(jī)制實(shí)現(xiàn)來(lái)替代 synchronized
。
其他說(shuō)明
最后,本文環(huán)境基于:
java version "14.0.1" 2020-04-14 Java(TM) SE Runtime Environment (build 14.0.1+7) Java HotSpot(TM) 64-Bit Server VM (build 14.0.1+7, mixed mode, sharing) JDK version 1.8.0_312
到此這篇關(guān)于Java多線程并發(fā)synchronized 關(guān)鍵字的文章就介紹到這了,更多相關(guān)Java synchronized內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
maven引入本地jar包運(yùn)行報(bào)錯(cuò)java.lang.NoClassDefFoundError解決
這篇文章主要為大家介紹了maven引入本地jar包運(yùn)行報(bào)錯(cuò)java.lang.NoClassDefFoundError解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10Spring Boot Event Bus用法小結(jié)
Spring Boot Event Bus是Spring框架中事件驅(qū)動(dòng)編程的一部分,本文主要介紹了Spring Boot Event Bus用法小結(jié),感興趣的可以了解一下2023-09-09springmvc 分頁(yè)查詢的簡(jiǎn)單實(shí)現(xiàn)示例代碼
我們?cè)陂_(kāi)發(fā)項(xiàng)目中很多項(xiàng)目都用到列表分頁(yè)功能,本篇介紹了springmvc 分頁(yè)查詢的簡(jiǎn)單實(shí)現(xiàn)示例代碼,非常具有實(shí)用價(jià)值,需要的朋友可以參考下。2017-01-01使用java + selenium + OpenCV破解騰訊防水墻滑動(dòng)驗(yàn)證碼功能
這篇文章主要介紹了使用java + selenium + OpenCV破解騰訊防水墻滑動(dòng)驗(yàn)證碼,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-11-11Java字符串拼接的五種方法及性能比較分析(從執(zhí)行100次到90萬(wàn)次)
字符串拼接一般使用“+”,但是“+”不能滿足大批量數(shù)據(jù)的處理,Java中有以下五種方法處理字符串拼接及性能比較分析,感興趣的可以了解一下2021-12-12Java字符編碼簡(jiǎn)介_(kāi)動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java字符編碼簡(jiǎn)介,本文主要包括以下幾個(gè)方面:編碼基本知識(shí),Java,系統(tǒng)軟件,url,工具軟件等,感興趣的朋友一起看看吧2017-08-08java 異常之手動(dòng)拋出與自動(dòng)拋出的實(shí)例講解
這篇文章主要介紹了java 異常之手動(dòng)拋出與自動(dòng)拋出的實(shí)例講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-02-02spring boot集成mongodb的增刪改查的示例代碼
這篇文章主要介紹了spring boot集成mongodb的增刪改查的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03