Java并發(fā)編程之synchronized底層實(shí)現(xiàn)原理分析
一、為什么出現(xiàn)synchronized
對(duì)于程序員而言,不管是在平常的工作中還是面試中,都會(huì)經(jīng)常用到或者被問(wèn)到synchronized。在多線程并發(fā)編程中,synchronized早已是元老級(jí)的角色了,很多人都稱其為重量級(jí)鎖,但是隨著Java SE 1.6對(duì)其進(jìn)行各種優(yōu)化之后,便顯得不再是那么的重了。
也正是因?yàn)槎嗑€程并發(fā)的出現(xiàn),便產(chǎn)生了線程安全這樣的問(wèn)題,對(duì)于線程安全的主要原因如下:
- 存在共享數(shù)據(jù)(也稱臨界資源)
- 存在多條線程共同操作這些共享數(shù)據(jù)
而對(duì)于解決這樣的一個(gè)問(wèn)題的辦法是:同一時(shí)刻有且只有一條線程在操作共享數(shù)據(jù),其他線程必須等待該線程處理完數(shù)據(jù)后再對(duì)共享數(shù)據(jù)進(jìn)行操作
此時(shí)便產(chǎn)生了互斥鎖,互斥鎖的特性如下:
- 互斥性:即在同一時(shí)刻只允許一個(gè)線程持有某個(gè)對(duì)象鎖,通過(guò)這種特性來(lái)實(shí)現(xiàn)多線程協(xié)調(diào)機(jī)制,這樣在同一時(shí)刻只有一個(gè)線程對(duì)所需要的同步的代碼塊(復(fù)合操作)進(jìn)行訪問(wèn)?;コ庑砸渤蔀榱瞬僮鞯脑有?。
- 可見(jiàn)性:必須確保在鎖釋放之前,對(duì)共享變量所做的修改,對(duì)于隨后獲得該鎖的另一個(gè)線程可見(jiàn)(即在獲得鎖時(shí)應(yīng)獲得最新共享變量的值),否則另一個(gè)線程可能是在本地緩存的某個(gè)副本上繼續(xù)操作,從而引起數(shù)據(jù)不一致。
對(duì)于Java而言,synchronized關(guān)鍵字滿足了以上的要求。
二、實(shí)現(xiàn)原理
首先我們要知道synchronized鎖的不是代碼,鎖的是對(duì)象。
根據(jù)獲取的鎖的分類:獲取對(duì)象鎖和獲取類鎖:
獲取對(duì)像鎖的兩種方法
- 1.同步代碼塊(synchronized(this),synchronized(類實(shí)例對(duì)象)),鎖是小括號(hào)()的實(shí)例對(duì)象
- 2.同步非靜態(tài)方法(synchronized method),鎖是當(dāng)前對(duì)象是實(shí)例對(duì)象
獲取類鎖的兩種方法
- 1.同步代碼塊(synchronized(類.class)),鎖是小括號(hào)()中的類對(duì)象(Class對(duì)象)
- 2.同步靜態(tài)方法(synchronized static method),鎖是當(dāng)前對(duì)象的類對(duì)象(Class對(duì)象)
對(duì)象鎖和類鎖的總結(jié):有線程訪問(wèn)對(duì)象的同步代碼塊時(shí),另外的線程可以訪問(wèn)該兌對(duì)象的非同步代碼塊
- 若鎖住的是同一個(gè)對(duì)象,一個(gè)線程在訪問(wèn)對(duì)象的 同步代碼塊時(shí),另一個(gè)訪問(wèn)對(duì)象的同步代碼塊的線程會(huì)被阻塞
- 若鎖住的是同一個(gè)對(duì)象,一個(gè)線程在訪問(wèn)對(duì)象的 同步方法時(shí),另一個(gè)訪問(wèn)對(duì)象的同步方法的線程會(huì)被阻塞
- 若鎖住的是同一個(gè)對(duì)象,一個(gè)線程在訪問(wèn)對(duì)象的 同步代碼塊時(shí),另一個(gè)訪問(wèn)對(duì)象的同步方法的線程會(huì)被阻塞
- 同一個(gè)類的不同對(duì)象的對(duì)象鎖互不干擾
- 類鎖由于是一種特殊的對(duì)象鎖,因此表現(xiàn)和上述1,2,3,4一致,由于一個(gè)只有一把對(duì)象鎖,所以同一個(gè)類的不同對(duì)象使用類鎖,將是同步的
- 類鎖和對(duì)象鎖互不干擾
當(dāng)一個(gè)線程試圖訪問(wèn)同步代碼塊時(shí),它首先必須得到鎖,退出或者拋出異常時(shí)必須釋放鎖,那么鎖存在哪里呢?
我們先來(lái)看一段代碼:
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é)碼中可以看出,同步語(yǔ)句塊的實(shí)現(xiàn)是使用monitorenter和monitorexit指令的,monitorenter指向同步代碼塊的開(kāi)始位置,它首先去獲取PrintStream這個(gè)類,然后傳入“Hello”這個(gè)參數(shù),然后再調(diào)用PrintStream中的println()方法去打印,monitorexit指明同步代碼塊的結(jié)束位置,當(dāng)執(zhí)行到monitorenter時(shí),當(dāng)前線程將試圖獲取對(duì)象鎖所對(duì)應(yīng)的monitor的持有權(quán)。
同步方法:從上面代碼的syncTask()方法字節(jié)碼中看,這里面并沒(méi)monitorenter和monitorexit,且字節(jié)碼較短,其實(shí)這里方法的同步是隱式的,是無(wú)需通過(guò)字節(jié)碼指令控制,在上面可以看到一個(gè)“ACC_SYNCHRONIZED”這樣的一個(gè)訪問(wèn)標(biāo)志,用來(lái)區(qū)分一個(gè)方法是否是同步方法。當(dāng)方法調(diào)用時(shí),調(diào)用指令將會(huì)檢查ACC_SYNCHRONIZED是否被設(shè)置,如果被設(shè)置,當(dāng)前線程將會(huì)持有monitor,然后再執(zhí)行方法,最后不管方法是否正常完成都會(huì)釋放monitor。
三、實(shí)現(xiàn)synchronized的基礎(chǔ)
Java對(duì)象頭和monitor是實(shí)現(xiàn)synchronized的基礎(chǔ),下面將會(huì)說(shuō)說(shuō)關(guān)于Java對(duì)象頭和monitor。
Java對(duì)象頭:
hotspot虛擬機(jī)中,對(duì)象在內(nèi)存的布局分布分為3個(gè)部分:對(duì)象頭,實(shí)例數(shù)據(jù),和對(duì)齊填充。
對(duì)象頭的結(jié)構(gòu)如下:
| 虛擬機(jī)位數(shù) | 頭對(duì)象結(jié)構(gòu) | 說(shuō)明 |
|---|---|---|
| 32/64 bit | Mark Word | 默認(rèn)存儲(chǔ)對(duì)象的hashCode,分代年齡,鎖類型,鎖標(biāo)志位等信息 |
| 32/64 bit | Class Metadata | 類型指針指向?qū)ο蟮念愒獢?shù)據(jù),JVM通過(guò)這個(gè)指針確定該對(duì)象是哪個(gè)類型的數(shù)據(jù) |
| 32/64 bit | Array length | 數(shù)組的長(zhǎng)度(如果當(dāng)前的對(duì)象是數(shù)組 ) |
mark word 被設(shè)計(jì)為非固定的數(shù)據(jù)結(jié)構(gòu),以便在極小的空間內(nèi)存儲(chǔ)更多的信息,它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間。
例如,在32位的Hotspot虛擬機(jī)中,如果對(duì)象處于未被鎖定的情況下。
那么Mark Word 的32bit空間中有25bit用于存儲(chǔ)對(duì)象的哈希碼、4bit用于存儲(chǔ)對(duì)象的分代年齡、2bi用于t存儲(chǔ)鎖的標(biāo)記位、1bit固定為0,而在其他的狀態(tài)下(輕量級(jí)鎖定、重量級(jí)鎖定、GC標(biāo)記、可偏向)下對(duì)象的存儲(chǔ)結(jié)構(gòu)如下:
| 存儲(chǔ)內(nèi)容 | 標(biāo)志位 | 狀態(tài) |
|---|---|---|
| 對(duì)象哈希嗎、對(duì)象分代年齡 | 01 | 未鎖定 |
| 指向鎖記錄的指針 | 00 | 輕量級(jí)鎖定 |
| 指向重量級(jí)鎖的指針 | 10 | 膨脹(重量級(jí)鎖定) |
| 空,不需要記錄信息 | 11 | GC標(biāo)記 |
| 偏向線程ID、偏向時(shí)間戳、對(duì)象分代年齡 | 01 | 可偏向 |
monitor:
每個(gè)Java對(duì)象天生就自帶了一把看不見(jiàn)的鎖,它可以視為是一種同步工具或者是一種同步機(jī)制,monitor還是線程私有的數(shù)據(jù)結(jié)構(gòu),每一個(gè)線程都有一個(gè)可用monitor 列表,同時(shí)還有一個(gè)全局的可用列表,如上面所說(shuō)每一個(gè)被鎖住的對(duì)象都會(huì)持有一個(gè)monitor。
monitor結(jié)構(gòu)如下:

- Owner:初始時(shí)為NULL表示當(dāng)前沒(méi)有任何線程擁有該monitor record,當(dāng)線程成功擁有該鎖后保存線程唯一標(biāo)識(shí),當(dāng)鎖被釋放時(shí)又設(shè)置為NULL;
- EntryQ:關(guān)聯(lián)一個(gè)系統(tǒng)互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的線程。
- RcThis:表示blocked或waiting在該monitor record上的所有線程的個(gè)數(shù)。
- Nest:用來(lái)實(shí)現(xiàn)重入鎖的計(jì)數(shù)。
- HashCode:保存從對(duì)象頭拷貝過(guò)來(lái)的HashCode值(可能還包含GC age)。
- Candidate:用來(lái)避免不必要的阻塞或等待線程喚醒,因?yàn)槊恳淮沃挥幸粋€(gè)線程能夠成功擁有鎖,如果每次前一個(gè)釋放鎖的線程喚醒所有正在阻塞或等待的線程,會(huì)引起不必要的上下文切換(從阻塞到就緒然后因?yàn)楦?jìng)爭(zhēng)鎖失敗又被阻塞)從而導(dǎo)致性能嚴(yán)重下降。Candidate只有兩種可能的值0表示沒(méi)有需要喚醒的線程1表示要喚醒一個(gè)繼任線程來(lái)競(jìng)爭(zhēng)鎖。
四、鎖優(yōu)化
Java6以后,對(duì)鎖進(jìn)行了大量的優(yōu)化,例如:AdaptiveSpinning(自適應(yīng)自旋)、Lock Eliminate(鎖消除)、Lock Coarsening(鎖粗化)、Lightweight Locking(輕量級(jí)鎖)、Biased Locking(偏向鎖)等等
自旋鎖
- 許多情況下,共享數(shù)據(jù)的鎖定狀態(tài)持續(xù)時(shí)間較短,切換線程不值得
- 通過(guò)讓線程執(zhí)行忙循環(huán)等待鎖的釋放,不讓出CPU
- 缺點(diǎn):若鎖被其他線程長(zhǎng)時(shí)間占用,會(huì)帶來(lái)許多性能上的開(kāi)銷
定義:所謂的自旋鎖就是讓沒(méi)有獲取到鎖的線程繼續(xù)等待一會(huì)兒,但不放棄CPU的執(zhí)行時(shí)間,這是的等一會(huì)和不放棄CPU的時(shí)間即是自旋鎖。
自適應(yīng)自旋鎖
- 自旋的次數(shù)不再固定
- 由前一次在同一鎖上的自旋時(shí)間及鎖擁有者的狀態(tài)來(lái)決定
鎖消除
鎖消除是對(duì)鎖更徹底的優(yōu)化,JIT編譯時(shí),對(duì)運(yùn)行上下文進(jìn)行掃描,去除不可能存在競(jìng)爭(zhēng)的鎖
public class StringBufferWithoutSync {
public void add(String str1, String str2) {
//StringBuffer是線程安全,由于sb只會(huì)在append方法中使用,不可能被其他線程引用
//因此sb屬于不可能共享的資源,JVM會(huì)自動(dòng)消除內(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()方法只會(huì)在內(nèi)部使用的,也就不可能被其他線程引用,因此對(duì)于這個(gè)變量sb而言,便不屬于共享資源了,JVM會(huì)自動(dòng)消除內(nèi)部的鎖。
鎖粗化
原則上我們都知道,在加同步鎖的時(shí)候,盡可能將同步塊的作用范圍先知道盡量小的范圍,及只在共享數(shù)據(jù)的實(shí)際工作范圍中進(jìn)行同步,這樣是為了需要同步的數(shù)量盡可能的變小,在存在鎖同步競(jìng)爭(zhēng)中,使得等待鎖的時(shí)間減小。
上述情況,大部分時(shí)候是正確的,但是如果存在一系列頻繁的操作,對(duì)同一個(gè)對(duì)象反復(fù)的加鎖、解鎖, 甚至加鎖時(shí)是在循環(huán)體中操作的,這樣即使沒(méi)有線程競(jìng)爭(zhēng),頻繁的進(jìn)行互斥鎖操作,也是導(dǎo)致不必要的性能開(kāi)銷。
對(duì)于解決這樣的問(wèn)題,我們只有盡可能的擴(kuò)大加鎖的范圍,例如下面的循環(huán)100次append,JVM會(huì)自己檢測(cè)到這樣的一個(gè)問(wèn)題,就會(huì)將加鎖的次數(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)分別為:無(wú)鎖、偏向鎖、輕量級(jí)鎖、重量級(jí)鎖
鎖的膨脹方向:無(wú)鎖——>偏向鎖——>輕量級(jí)鎖——>重量級(jí)鎖
偏向鎖
作用:減少同一線程獲取鎖的代價(jià)
引入偏向鎖是因?yàn)榇蠖鄶?shù)情況下,鎖并不存在多線程競(jìng)爭(zhēng),總是由同一線程多次獲得
獲取鎖
- 檢測(cè)Mark Word是否為可偏向狀態(tài),即是否為偏向鎖1,鎖標(biāo)識(shí)位為01;
- 若為可偏向狀態(tài),則測(cè)試線程ID是否為當(dāng)前線程ID,如果是,則執(zhí)行步驟(5),否則執(zhí)行步驟(3);
- 如果線程ID不為當(dāng)前線程ID,則通過(guò)CAS操作競(jìng)爭(zhēng)鎖,競(jìng)爭(zhēng)成功,則將Mark Word的線程ID替換為當(dāng)前線程ID,否則執(zhí)行線程(4);
- 通過(guò)CAS競(jìng)爭(zhēng)鎖失敗,證明當(dāng)前存在多線程競(jìng)爭(zhēng)情況,當(dāng)?shù)竭_(dá)全局安全點(diǎn),獲得偏向鎖的線程被掛起,偏向鎖升級(jí)為輕量級(jí)鎖,然后被阻塞在安全點(diǎn)的線程繼續(xù)往下執(zhí)行同步代碼塊;
- 執(zhí)行同步代碼塊
釋放鎖
偏向鎖的釋放采用了一種只有競(jìng)爭(zhēng)才會(huì)釋放鎖的機(jī)制,線程是不會(huì)主動(dòng)去釋放偏向鎖,需要等待其他線程來(lái)競(jìng)爭(zhēng)。偏向鎖的撤銷需要等待全局安全點(diǎn)(這個(gè)時(shí)間點(diǎn)是上沒(méi)有正在執(zhí)行的代碼)。
其步驟如下:
- 暫停擁有偏向鎖的線程,判斷鎖對(duì)象石是否還處于被鎖定狀態(tài);
- 撤銷偏向蘇,恢復(fù)到無(wú)鎖狀態(tài)(01)或者輕量級(jí)鎖的狀態(tài);

輕量級(jí)鎖
獲取鎖
- 判斷當(dāng)前對(duì)象是否處于無(wú)鎖狀態(tài)(hashcode、0、01),若是,則JVM首先將在當(dāng)前線程的棧幀中建立一個(gè)名為鎖記錄(Lock Record)的空間,用于存儲(chǔ)鎖對(duì)象目前的Mark Word的拷貝(官方把這份拷貝加了一個(gè)
- Displaced前綴,即Displaced Mark Word);否則執(zhí)行步驟(3);
- JVM利用CAS操作嘗試將對(duì)象的Mark Word更新為指向Lock Record的指正,如果成功表示競(jìng)爭(zhēng)到鎖,則將鎖標(biāo)志位變成00(表示此對(duì)象處于輕量級(jí)鎖狀態(tài)),執(zhí)行同步操作;如果失敗則執(zhí)行步驟(3);
- 判斷當(dāng)前對(duì)象的Mark Word是否指向當(dāng)前線程的棧幀,如果是則表示當(dāng)前線程已經(jīng)持有當(dāng)前對(duì)象的鎖,則直接執(zhí)行同步代碼塊;否則只能說(shuō)明該鎖對(duì)象已經(jīng)被其他線程搶占了,這時(shí)輕量級(jí)鎖需要膨脹為重量級(jí)鎖,鎖
- 標(biāo)志位變成10,后面等待的線程將會(huì)進(jìn)入阻塞狀態(tài);
釋放鎖
輕量級(jí)鎖的釋放也是通過(guò)CAS操作來(lái)進(jìn)行的,主要步驟如下:
- 取出在獲取輕量級(jí)鎖保存在Displaced Mark Word中的數(shù)據(jù);
- 用CAS操作將取出的數(shù)據(jù)替換當(dāng)前對(duì)象的Mark Word中,如果成功,則說(shuō)明釋放鎖成功,否則執(zhí)行(3);
- 如果CAS操作替換失敗,說(shuō)明有其他線程嘗試獲取該鎖,則需要在釋放鎖的同時(shí)需要喚醒被掛起的線程。
對(duì)于輕量級(jí)鎖,其性能提升的依據(jù)是“對(duì)于絕大部分的鎖,在整個(gè)生命周期內(nèi)都是不會(huì)存在競(jìng)爭(zhēng)的”,如果打破這個(gè)依據(jù)則除了互斥的開(kāi)銷外,還有額外的CAS操作,因此在有多線程競(jìng)爭(zhēng)的情況下,輕量級(jí)鎖比重量級(jí)鎖更慢;

重量級(jí)鎖
重量級(jí)鎖通過(guò)對(duì)象內(nèi)部的監(jiān)視器(monitor)實(shí)現(xiàn),其中monitor的本質(zhì)是依賴于底層操作系統(tǒng)的Mutex Lock實(shí)現(xiàn),操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換,切換成本非常高。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot如何優(yōu)雅地使用Swagger2
這篇文章主要介紹了SpringBoot如何優(yōu)雅地使用Swagger2,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07
java使用BeanUtils.copyProperties踩坑經(jīng)歷
最近在做個(gè)項(xiàng)目,踩了個(gè)坑特此記錄一下,本文主要介紹了使用BeanUtils.copyProperties踩坑經(jīng)歷,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05
解決springboot整合cxf-jaxrs中json轉(zhuǎn)換的問(wèn)題
這篇文章主要介紹了解決springboot整合cxf-jaxrs中json轉(zhuǎn)換的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
淺析Java關(guān)鍵詞synchronized的使用
Synchronized是java虛擬機(jī)為線程安全而引入的。這篇文章主要為大家介紹一下Java關(guān)鍵詞synchronized的使用與原理,需要的可以參考一下2022-12-12
Idea調(diào)用WebService的關(guān)鍵步驟和注意事項(xiàng)
這篇文章主要介紹了如何在Idea中調(diào)用WebService,包括理解WebService的基本概念、獲取WSDL文件、閱讀和理解WSDL文件、選擇對(duì)接測(cè)試工具或方式、發(fā)送請(qǐng)求和接收響應(yīng)、處理響應(yīng)結(jié)果以及錯(cuò)誤處理,需要的朋友可以參考下2025-01-01
SpringShell命令行之交互式Shell應(yīng)用開(kāi)發(fā)方式
本文將深入探討Spring Shell的核心特性、實(shí)現(xiàn)方式及應(yīng)用場(chǎng)景,幫助開(kāi)發(fā)者掌握這一強(qiáng)大工具,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-04-04
Java獲取漢字拼音的全拼和首拼實(shí)現(xiàn)代碼分享
這篇文章主要介紹了Java獲取漢字拼音的全拼和首拼實(shí)現(xiàn)代碼分享,本文直接給出實(shí)現(xiàn)代碼,需要的朋友可以參考下2015-06-06
SpringBoot中maven項(xiàng)目打成war包部署在linux服務(wù)器上的方法
這篇文章主要介紹了SpringBoot中maven項(xiàng)目打成war包部署在linux服務(wù)器上的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05

