Java并發(fā)編程之synchronized底層實現(xiàn)原理分析
一、為什么出現(xiàn)synchronized
對于程序員而言,不管是在平常的工作中還是面試中,都會經(jīng)常用到或者被問到synchronized。在多線程并發(fā)編程中,synchronized早已是元老級的角色了,很多人都稱其為重量級鎖,但是隨著Java SE 1.6對其進(jìn)行各種優(yōu)化之后,便顯得不再是那么的重了。
也正是因為多線程并發(fā)的出現(xiàn),便產(chǎn)生了線程安全這樣的問題,對于線程安全的主要原因如下:
- 存在共享數(shù)據(jù)(也稱臨界資源)
- 存在多條線程共同操作這些共享數(shù)據(jù)
而對于解決這樣的一個問題的辦法是:同一時刻有且只有一條線程在操作共享數(shù)據(jù),其他線程必須等待該線程處理完數(shù)據(jù)后再對共享數(shù)據(jù)進(jìn)行操作
此時便產(chǎn)生了互斥鎖,互斥鎖的特性如下:
- 互斥性:即在同一時刻只允許一個線程持有某個對象鎖,通過這種特性來實現(xiàn)多線程協(xié)調(diào)機制,這樣在同一時刻只有一個線程對所需要的同步的代碼塊(復(fù)合操作)進(jìn)行訪問?;コ庑砸渤蔀榱瞬僮鞯脑有?。
- 可見性:必須確保在鎖釋放之前,對共享變量所做的修改,對于隨后獲得該鎖的另一個線程可見(即在獲得鎖時應(yīng)獲得最新共享變量的值),否則另一個線程可能是在本地緩存的某個副本上繼續(xù)操作,從而引起數(shù)據(jù)不一致。
對于Java而言,synchronized關(guān)鍵字滿足了以上的要求。
二、實現(xiàn)原理
首先我們要知道synchronized鎖的不是代碼,鎖的是對象。
根據(jù)獲取的鎖的分類:獲取對象鎖和獲取類鎖:
獲取對像鎖的兩種方法
- 1.同步代碼塊(synchronized(this),synchronized(類實例對象)),鎖是小括號()的實例對象
- 2.同步非靜態(tài)方法(synchronized method),鎖是當(dāng)前對象是實例對象
獲取類鎖的兩種方法
- 1.同步代碼塊(synchronized(類.class)),鎖是小括號()中的類對象(Class對象)
- 2.同步靜態(tài)方法(synchronized static method),鎖是當(dāng)前對象的類對象(Class對象)
對象鎖和類鎖的總結(jié):有線程訪問對象的同步代碼塊時,另外的線程可以訪問該兌對象的非同步代碼塊
- 若鎖住的是同一個對象,一個線程在訪問對象的 同步代碼塊時,另一個訪問對象的同步代碼塊的線程會被阻塞
- 若鎖住的是同一個對象,一個線程在訪問對象的 同步方法時,另一個訪問對象的同步方法的線程會被阻塞
- 若鎖住的是同一個對象,一個線程在訪問對象的 同步代碼塊時,另一個訪問對象的同步方法的線程會被阻塞
- 同一個類的不同對象的對象鎖互不干擾
- 類鎖由于是一種特殊的對象鎖,因此表現(xiàn)和上述1,2,3,4一致,由于一個只有一把對象鎖,所以同一個類的不同對象使用類鎖,將是同步的
- 類鎖和對象鎖互不干擾
當(dāng)一個線程試圖訪問同步代碼塊時,它首先必須得到鎖,退出或者拋出異常時必須釋放鎖,那么鎖存在哪里呢?
我們先來看一段代碼:
public class SyncBlockTest { public void syncsTask() { synchronized (this) { System.out.println("Hello"); } } public synchronized void syncTask() { System.out.println("Hello Baby"); } }
在使用javac工具把上面代碼變異成class,然后使用javap工具查看編譯好的class文件,如下:
public com.interview.javabasic.thread.SyncBlockTest(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 public void syncsTask(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=3, args_size=1 0: aload_0 1: dup 2: astore_1 3: monitorenter 4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 7: ldc #3 // String Hello 9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 12: aload_1 13: monitorexit 14: goto 22 17: astore_2 18: aload_1 19: monitorexit 20: aload_2 21: athrow 22: return Exception table: from to target type 4 14 17 any 17 20 17 any LineNumberTable: line 10: 0 line 11: 4 line 12: 12 line 13: 22 StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 17 locals = [ class com/interview/javabasic/thread/SyncBlockTest, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 public synchronized void syncTask(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED Code: stack=2, locals=1, args_size=1 0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #5 // String Hello Baby 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 16: 0 line 17: 8 }
同步代碼塊:從上面的字節(jié)碼中可以看出,同步語句塊的實現(xiàn)是使用monitorenter和monitorexit指令的,monitorenter指向同步代碼塊的開始位置,它首先去獲取PrintStream這個類,然后傳入“Hello”這個參數(shù),然后再調(diào)用PrintStream中的println()方法去打印,monitorexit指明同步代碼塊的結(jié)束位置,當(dāng)執(zhí)行到monitorenter時,當(dāng)前線程將試圖獲取對象鎖所對應(yīng)的monitor的持有權(quán)。
同步方法:從上面代碼的syncTask()方法字節(jié)碼中看,這里面并沒monitorenter和monitorexit,且字節(jié)碼較短,其實這里方法的同步是隱式的,是無需通過字節(jié)碼指令控制,在上面可以看到一個“ACC_SYNCHRONIZED”這樣的一個訪問標(biāo)志,用來區(qū)分一個方法是否是同步方法。當(dāng)方法調(diào)用時,調(diào)用指令將會檢查ACC_SYNCHRONIZED是否被設(shè)置,如果被設(shè)置,當(dāng)前線程將會持有monitor,然后再執(zhí)行方法,最后不管方法是否正常完成都會釋放monitor。
三、實現(xiàn)synchronized的基礎(chǔ)
Java對象頭和monitor是實現(xiàn)synchronized的基礎(chǔ),下面將會說說關(guān)于Java對象頭和monitor。
Java對象頭:
hotspot虛擬機中,對象在內(nèi)存的布局分布分為3個部分:對象頭,實例數(shù)據(jù),和對齊填充。
對象頭的結(jié)構(gòu)如下:
虛擬機位數(shù) | 頭對象結(jié)構(gòu) | 說明 |
---|---|---|
32/64 bit | Mark Word | 默認(rèn)存儲對象的hashCode,分代年齡,鎖類型,鎖標(biāo)志位等信息 |
32/64 bit | Class Metadata | 類型指針指向?qū)ο蟮念愒獢?shù)據(jù),JVM通過這個指針確定該對象是哪個類型的數(shù)據(jù) |
32/64 bit | Array length | 數(shù)組的長度(如果當(dāng)前的對象是數(shù)組 ) |
mark word 被設(shè)計為非固定的數(shù)據(jù)結(jié)構(gòu),以便在極小的空間內(nèi)存儲更多的信息,它會根據(jù)對象的狀態(tài)復(fù)用自己的存儲空間。
例如,在32位的Hotspot虛擬機中,如果對象處于未被鎖定的情況下。
那么Mark Word 的32bit空間中有25bit用于存儲對象的哈希碼、4bit用于存儲對象的分代年齡、2bi用于t存儲鎖的標(biāo)記位、1bit固定為0,而在其他的狀態(tài)下(輕量級鎖定、重量級鎖定、GC標(biāo)記、可偏向)下對象的存儲結(jié)構(gòu)如下:
存儲內(nèi)容 | 標(biāo)志位 | 狀態(tài) |
---|---|---|
對象哈希嗎、對象分代年齡 | 01 | 未鎖定 |
指向鎖記錄的指針 | 00 | 輕量級鎖定 |
指向重量級鎖的指針 | 10 | 膨脹(重量級鎖定) |
空,不需要記錄信息 | 11 | GC標(biāo)記 |
偏向線程ID、偏向時間戳、對象分代年齡 | 01 | 可偏向 |
monitor:
每個Java對象天生就自帶了一把看不見的鎖,它可以視為是一種同步工具或者是一種同步機制,monitor還是線程私有的數(shù)據(jù)結(jié)構(gòu),每一個線程都有一個可用monitor 列表,同時還有一個全局的可用列表,如上面所說每一個被鎖住的對象都會持有一個monitor。
monitor結(jié)構(gòu)如下:
- Owner:初始時為NULL表示當(dāng)前沒有任何線程擁有該monitor record,當(dāng)線程成功擁有該鎖后保存線程唯一標(biāo)識,當(dāng)鎖被釋放時又設(shè)置為NULL;
- EntryQ:關(guān)聯(lián)一個系統(tǒng)互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的線程。
- RcThis:表示blocked或waiting在該monitor record上的所有線程的個數(shù)。
- Nest:用來實現(xiàn)重入鎖的計數(shù)。
- HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)。
- Candidate:用來避免不必要的阻塞或等待線程喚醒,因為每一次只有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然后因為競爭鎖失敗又被阻塞)從而導(dǎo)致性能嚴(yán)重下降。Candidate只有兩種可能的值0表示沒有需要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖。
四、鎖優(yōu)化
Java6以后,對鎖進(jìn)行了大量的優(yōu)化,例如:AdaptiveSpinning(自適應(yīng)自旋)、Lock Eliminate(鎖消除)、Lock Coarsening(鎖粗化)、Lightweight Locking(輕量級鎖)、Biased Locking(偏向鎖)等等
自旋鎖
- 許多情況下,共享數(shù)據(jù)的鎖定狀態(tài)持續(xù)時間較短,切換線程不值得
- 通過讓線程執(zhí)行忙循環(huán)等待鎖的釋放,不讓出CPU
- 缺點:若鎖被其他線程長時間占用,會帶來許多性能上的開銷
定義:所謂的自旋鎖就是讓沒有獲取到鎖的線程繼續(xù)等待一會兒,但不放棄CPU的執(zhí)行時間,這是的等一會和不放棄CPU的時間即是自旋鎖。
自適應(yīng)自旋鎖
- 自旋的次數(shù)不再固定
- 由前一次在同一鎖上的自旋時間及鎖擁有者的狀態(tài)來決定
鎖消除
鎖消除是對鎖更徹底的優(yōu)化,JIT編譯時,對運行上下文進(jìn)行掃描,去除不可能存在競爭的鎖
public class StringBufferWithoutSync { public void add(String str1, String str2) { //StringBuffer是線程安全,由于sb只會在append方法中使用,不可能被其他線程引用 //因此sb屬于不可能共享的資源,JVM會自動消除內(nèi)部的鎖 StringBuffer sb = new StringBuffer(); sb.append(str1).append(str2); } public static void main(String[] args) { StringBufferWithoutSync withoutSync = new StringBufferWithoutSync(); for (int i = 0; i < 1000; i++) { withoutSync.add("aaa", "bbb"); } } }
例如Java中的StringBuffer 它是線程安全的,由于其append()方法只會在內(nèi)部使用的,也就不可能被其他線程引用,因此對于這個變量sb而言,便不屬于共享資源了,JVM會自動消除內(nèi)部的鎖。
鎖粗化
原則上我們都知道,在加同步鎖的時候,盡可能將同步塊的作用范圍先知道盡量小的范圍,及只在共享數(shù)據(jù)的實際工作范圍中進(jìn)行同步,這樣是為了需要同步的數(shù)量盡可能的變小,在存在鎖同步競爭中,使得等待鎖的時間減小。
上述情況,大部分時候是正確的,但是如果存在一系列頻繁的操作,對同一個對象反復(fù)的加鎖、解鎖, 甚至加鎖時是在循環(huán)體中操作的,這樣即使沒有線程競爭,頻繁的進(jìn)行互斥鎖操作,也是導(dǎo)致不必要的性能開銷。
對于解決這樣的問題,我們只有盡可能的擴大加鎖的范圍,例如下面的循環(huán)100次append,JVM會自己檢測到這樣的一個問題,就會將加鎖的次數(shù)減至一次。
public static String copyString(String target){ int i = 0; StringBuffer sb = new StringBuffer(); while (i<100){ sb.append(target); } return sb.toString(); }
五、synchronized鎖的狀態(tài)
synchronized鎖有四種狀態(tài)分別為:無鎖、偏向鎖、輕量級鎖、重量級鎖
鎖的膨脹方向:無鎖——>偏向鎖——>輕量級鎖——>重量級鎖
偏向鎖
作用:減少同一線程獲取鎖的代價
引入偏向鎖是因為大多數(shù)情況下,鎖并不存在多線程競爭,總是由同一線程多次獲得
獲取鎖
- 檢測Mark Word是否為可偏向狀態(tài),即是否為偏向鎖1,鎖標(biāo)識位為01;
- 若為可偏向狀態(tài),則測試線程ID是否為當(dāng)前線程ID,如果是,則執(zhí)行步驟(5),否則執(zhí)行步驟(3);
- 如果線程ID不為當(dāng)前線程ID,則通過CAS操作競爭鎖,競爭成功,則將Mark Word的線程ID替換為當(dāng)前線程ID,否則執(zhí)行線程(4);
- 通過CAS競爭鎖失敗,證明當(dāng)前存在多線程競爭情況,當(dāng)?shù)竭_(dá)全局安全點,獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續(xù)往下執(zhí)行同步代碼塊;
- 執(zhí)行同步代碼塊
釋放鎖
偏向鎖的釋放采用了一種只有競爭才會釋放鎖的機制,線程是不會主動去釋放偏向鎖,需要等待其他線程來競爭。偏向鎖的撤銷需要等待全局安全點(這個時間點是上沒有正在執(zhí)行的代碼)。
其步驟如下:
- 暫停擁有偏向鎖的線程,判斷鎖對象石是否還處于被鎖定狀態(tài);
- 撤銷偏向蘇,恢復(fù)到無鎖狀態(tài)(01)或者輕量級鎖的狀態(tài);
輕量級鎖
獲取鎖
- 判斷當(dāng)前對象是否處于無鎖狀態(tài)(hashcode、0、01),若是,則JVM首先將在當(dāng)前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了一個
- Displaced前綴,即Displaced Mark Word);否則執(zhí)行步驟(3);
- JVM利用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指正,如果成功表示競爭到鎖,則將鎖標(biāo)志位變成00(表示此對象處于輕量級鎖狀態(tài)),執(zhí)行同步操作;如果失敗則執(zhí)行步驟(3);
- 判斷當(dāng)前對象的Mark Word是否指向當(dāng)前線程的棧幀,如果是則表示當(dāng)前線程已經(jīng)持有當(dāng)前對象的鎖,則直接執(zhí)行同步代碼塊;否則只能說明該鎖對象已經(jīng)被其他線程搶占了,這時輕量級鎖需要膨脹為重量級鎖,鎖
- 標(biāo)志位變成10,后面等待的線程將會進(jìn)入阻塞狀態(tài);
釋放鎖
輕量級鎖的釋放也是通過CAS操作來進(jìn)行的,主要步驟如下:
- 取出在獲取輕量級鎖保存在Displaced Mark Word中的數(shù)據(jù);
- 用CAS操作將取出的數(shù)據(jù)替換當(dāng)前對象的Mark Word中,如果成功,則說明釋放鎖成功,否則執(zhí)行(3);
- 如果CAS操作替換失敗,說明有其他線程嘗試獲取該鎖,則需要在釋放鎖的同時需要喚醒被掛起的線程。
對于輕量級鎖,其性能提升的依據(jù)是“對于絕大部分的鎖,在整個生命周期內(nèi)都是不會存在競爭的”,如果打破這個依據(jù)則除了互斥的開銷外,還有額外的CAS操作,因此在有多線程競爭的情況下,輕量級鎖比重量級鎖更慢;
重量級鎖
重量級鎖通過對象內(nèi)部的監(jiān)視器(monitor)實現(xiàn),其中monitor的本質(zhì)是依賴于底層操作系統(tǒng)的Mutex Lock實現(xiàn),操作系統(tǒng)實現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換,切換成本非常高。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot如何優(yōu)雅地使用Swagger2
這篇文章主要介紹了SpringBoot如何優(yōu)雅地使用Swagger2,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-07-07java使用BeanUtils.copyProperties踩坑經(jīng)歷
最近在做個項目,踩了個坑特此記錄一下,本文主要介紹了使用BeanUtils.copyProperties踩坑經(jīng)歷,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05解決springboot整合cxf-jaxrs中json轉(zhuǎn)換的問題
這篇文章主要介紹了解決springboot整合cxf-jaxrs中json轉(zhuǎn)換的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07淺析Java關(guān)鍵詞synchronized的使用
Synchronized是java虛擬機為線程安全而引入的。這篇文章主要為大家介紹一下Java關(guān)鍵詞synchronized的使用與原理,需要的可以參考一下2022-12-12Idea調(diào)用WebService的關(guān)鍵步驟和注意事項
這篇文章主要介紹了如何在Idea中調(diào)用WebService,包括理解WebService的基本概念、獲取WSDL文件、閱讀和理解WSDL文件、選擇對接測試工具或方式、發(fā)送請求和接收響應(yīng)、處理響應(yīng)結(jié)果以及錯誤處理,需要的朋友可以參考下2025-01-01SpringShell命令行之交互式Shell應(yīng)用開發(fā)方式
本文將深入探討Spring Shell的核心特性、實現(xiàn)方式及應(yīng)用場景,幫助開發(fā)者掌握這一強大工具,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-04-04SpringBoot中maven項目打成war包部署在linux服務(wù)器上的方法
這篇文章主要介紹了SpringBoot中maven項目打成war包部署在linux服務(wù)器上的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05