Java并發(fā)編程中的synchronized關(guān)鍵字詳細(xì)解讀
前言
在 Java 早期版本中,synchronized 屬于 重量級(jí)鎖,效率低下。這是因?yàn)楸O(jiān)視器鎖(monitor)是依賴于底層的操作系統(tǒng)的 Mutex Lock 來實(shí)現(xiàn)的,Java 的線程是映射到操作系統(tǒng)的原生線程之上的。如果要掛起或者喚醒一個(gè)線程,都需要操作系統(tǒng)幫忙完成,而操作系統(tǒng)實(shí)現(xiàn)線程之間的切換時(shí)需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),這個(gè)狀態(tài)之間的轉(zhuǎn)換需要相對(duì)比較長(zhǎng)的時(shí)間,時(shí)間成本相對(duì)較高。在 Java 6 之后, synchronized 引入了大量的優(yōu)化如自旋鎖、適應(yīng)性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級(jí)鎖等技術(shù)來減少鎖操作的開銷,這些優(yōu)化讓 synchronized 鎖的效率提升了很多。
一、synchronized的使用方法
synchronized 塊是 Java 提供的一種原子性內(nèi)置鎖,Java 中的每個(gè)對(duì)象都可以把它當(dāng)作一個(gè)同步鎖來使用。
synchronized關(guān)鍵字可以用來修飾實(shí)例方法、靜態(tài)方法、代碼塊,表示對(duì)其進(jìn)行加鎖,當(dāng)線程進(jìn)入 synchronized 代碼塊前只有獲取到相應(yīng)的鎖才能訪問,否則自動(dòng)進(jìn)入自旋或阻塞狀態(tài)(BLOCKED)等待鎖被其他線程釋放后競(jìng)爭(zhēng)鎖。
1、修飾實(shí)例方法 (鎖當(dāng)前對(duì)象實(shí)例)
給當(dāng)前對(duì)象實(shí)例加鎖,進(jìn)入同步代碼前要獲得 當(dāng)前對(duì)象實(shí)例的鎖 。
synchronized void method() {
//業(yè)務(wù)代碼
}2、修飾靜態(tài)方法 (鎖類對(duì)象)
給當(dāng)前類加鎖,進(jìn)入同步代碼前要獲得 當(dāng)前類class對(duì)象的鎖。這是因?yàn)殪o態(tài)成員不屬于任何一個(gè)實(shí)例對(duì)象,歸整個(gè)類所有,不依賴于類的特定實(shí)例。
synchronized static void method() {
//業(yè)務(wù)代碼
}3、修飾代碼塊 (鎖指定對(duì)象/類對(duì)象)
- synchronized(object) 表示進(jìn)入同步代碼前要獲得 給定對(duì)象的鎖。
- synchronized(類.class) 表示進(jìn)入同步代碼前要獲得 給定 Class對(duì)象 的鎖。
synchronized(this) {
//業(yè)務(wù)代碼
}二、synchronized的特性
1. 可重入鎖
持有鎖的線程可直接進(jìn)入此鎖關(guān)聯(lián)的任意其他代碼。
2. 非公平鎖
不是按照先來后到的原則來分配鎖。
3. 不可中斷鎖
synchronized在鎖競(jìng)爭(zhēng)時(shí)是不可中斷的,獲取不到鎖的線程會(huì)一直處于阻塞狀態(tài)。而ReentrantLock獲取鎖失敗可以被interrupt()進(jìn)行中斷操作。
三、synchronized相關(guān)問題
1. volatile和synchronized的區(qū)別是什么?
- volatile 關(guān)鍵字用于修飾變量,可保證變量的可見性和有序性。
- synchronized關(guān)鍵字用于修飾方法或代碼塊,可保證代碼塊的原子性以及代碼塊內(nèi)變量的可見性,以及代碼塊外部和內(nèi)部之間的有序性(代碼塊內(nèi)部的有序性不保證,例如DCL單例指令重排問題)。
2. 占有鎖的線程在什么情況下會(huì)釋放鎖?
- 占有鎖的線程執(zhí)行完了該代碼塊,然后釋放對(duì)鎖的占有;
- 占有鎖線程執(zhí)行發(fā)生異常,此時(shí)JVM會(huì)讓線程自動(dòng)釋放鎖;
- 占有鎖線程進(jìn)入 WAITING 狀態(tài)從而釋放鎖,例如在該線程中調(diào)用wait()方法等
四、synchronized的底層原理
對(duì)于synchronized同步代碼塊,編譯后在代碼塊前后分別有一個(gè)monitorenter 和 monitorexit 指令,在JVM中當(dāng)線程執(zhí)行到monitorenter指令時(shí)嘗試獲取指定對(duì)象的鎖,執(zhí)行到monitorexit 指令則釋放鎖。

對(duì)于synchronized同步方法,編譯后方法中有一個(gè)ACC_SYNCHRONIZED標(biāo)識(shí),在JVM中當(dāng)線程執(zhí)行到有此標(biāo)識(shí)的方法時(shí)會(huì)隱式調(diào)用monitorenter和monitorexit。在執(zhí)行同步方法前會(huì)調(diào)用monitorenter,在執(zhí)行完同步方法后會(huì)調(diào)用monitorexit。最后都能達(dá)到加鎖的效果。

五、synchronized的鎖升級(jí)過程
早期synchronized實(shí)現(xiàn)的同步鎖為重量級(jí)鎖。但是重量級(jí)鎖會(huì)造成線程阻塞排隊(duì),阻塞和喚醒線程會(huì)使CPU在用戶態(tài)和核心態(tài)之間頻繁切換,所以代價(jià)高、效率低。因此 Java6 對(duì) synchronized 鎖進(jìn)行了優(yōu)化,增加了輕量級(jí)鎖和偏向鎖。為了提高效率,不會(huì)一開始就使用重量級(jí)鎖,JVM在內(nèi)部會(huì)根據(jù)需要,按如下步驟進(jìn)行鎖的升級(jí):

1. 無鎖狀態(tài)
初期鎖對(duì)象剛創(chuàng)建時(shí),還沒有任何線程來競(jìng)爭(zhēng),對(duì)象的markword是上圖的第一種情形,這偏向鎖標(biāo)識(shí)位是0,鎖狀態(tài)01,說明該對(duì)象處于無鎖狀態(tài)(無線程競(jìng)爭(zhēng)它)。
2. 偏向鎖
當(dāng)有一個(gè)線程來競(jìng)爭(zhēng)鎖時(shí),先用偏向鎖,會(huì)在對(duì)象頭的markword中記錄線程threadID,并且線程退出synchronized塊后偏向鎖不會(huì)主動(dòng)釋放,因此之后此線程需要再次獲取鎖的時(shí)候,通過比較當(dāng)前線程的 threadID 和 對(duì)象頭中的threadID 發(fā)現(xiàn)一致,就不需要再做任何檢查和切換直接進(jìn)入,這種競(jìng)爭(zhēng)不激烈的情況下,效率非常高。如上圖第二種情形。
JDK 15中的偏向鎖 偏向鎖在單線程反復(fù)獲取鎖的場(chǎng)景下性能很高,但細(xì)想便知生產(chǎn)環(huán)境中高并發(fā)的場(chǎng)景下很難有這種場(chǎng)景。 而且對(duì)于偏向鎖來說,在多線程競(jìng)爭(zhēng)時(shí)的撤銷操作十分復(fù)雜且?guī)砹祟~外的性能消耗(需要等到safe point,并STW)。 JDK 15 之前,偏向鎖默認(rèn)是 開啟的,從 JDK 15 開始,默認(rèn)就是關(guān)閉的了,需要顯式打開(-XX:+UseBiasedLocking)。
3. 輕量級(jí)鎖
當(dāng)需要獲取對(duì)象的hashcode值時(shí)就會(huì)禁用偏向鎖升級(jí)為輕量級(jí)鎖,將hashcode值寫入markword;或者當(dāng)有第二個(gè)線程開始競(jìng)爭(zhēng)這個(gè)鎖對(duì)象,通過對(duì)比markword中記錄線程threadID發(fā)現(xiàn)不一致,那么首先需要查看Java 對(duì)象頭中記錄的線程 1 是否存活(偏向鎖不會(huì)主動(dòng)釋放鎖),如果沒有存活,那么鎖對(duì)象被重置為無鎖狀態(tài),其它線程(線程 2)可以競(jìng)爭(zhēng)將其設(shè)置為偏向鎖;如果存活,那么立刻查找該線程(線程 1)的棧幀信息,如果還是需要繼續(xù)持有這個(gè)鎖對(duì)象,那么暫停當(dāng)前線程 1,撤銷偏向鎖,升級(jí)為 輕量級(jí)鎖,如果線程 1 不再使用該鎖對(duì)象,那么將鎖對(duì)象狀態(tài)設(shè)為無鎖狀態(tài),重新偏向新的線程。如上圖第三種情形。

4. 重量級(jí)鎖(monitor)
當(dāng)輕量級(jí)鎖等待獲取鎖的線程自旋到達(dá)一定次數(shù),或者競(jìng)爭(zhēng)的這個(gè)鎖對(duì)象的線程更多,導(dǎo)致了更多的切換和等待,JVM會(huì)把該鎖對(duì)象的鎖升級(jí)為重量級(jí)鎖。synchronized的重量級(jí)鎖是基于在監(jiān)視器(monitor)實(shí)現(xiàn)的,JVM中每個(gè)對(duì)象都可以關(guān)聯(lián)一個(gè)ObjectMonitor監(jiān)視器對(duì)象(C++實(shí)現(xiàn)),升級(jí)為重量級(jí)鎖后對(duì)象的Mark Word再次發(fā)生變化,會(huì)指向?qū)ο箨P(guān)聯(lián)的監(jiān)視器對(duì)象(如上圖第四種情形)。
ObjectMonitor的主要數(shù)據(jù)結(jié)構(gòu)如下:
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; //線程重入次數(shù)
_object = NULL; //指向?qū)?yīng)的java對(duì)象
_owner = NULL; //持有鎖的線程
_WaitSet = NULL; //等待隊(duì)列:處于wait狀態(tài)的線程會(huì)被加入到這個(gè)隊(duì)列中
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ; //要競(jìng)爭(zhēng)鎖的線程會(huì)先被加入到這個(gè)隊(duì)列中
FreeNext = NULL ;
_EntryList = NULL ; //處于blocked阻塞狀態(tài)的線程,會(huì)被加入到這個(gè)隊(duì)列中
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}鎖競(jìng)爭(zhēng)機(jī)制:
- 當(dāng)線程要獲取鎖時(shí),首先將其加入cxq隊(duì)列的頭部,獲取失敗被阻塞后則加入EntryList隊(duì)列。
- 當(dāng)持有鎖的線程釋放鎖時(shí),會(huì)根據(jù)策略喚醒cxq或者EntryList隊(duì)列中的線程來競(jìng)爭(zhēng)鎖。
- 當(dāng)線程調(diào)用wait()方法后會(huì)釋放鎖進(jìn)入阻塞狀態(tài)并加入waitSet等待隊(duì)列。當(dāng)調(diào)用notify方法后,會(huì)從waitSet中喚醒線程加入到cxq或者EntryList隊(duì)列。

六、synchronized的其它優(yōu)化
1. 鎖粗化
原則上,我們?cè)诰帉懘a的時(shí)候,總是推薦將同步塊的作用范圍限制得盡量小,只在共享數(shù)據(jù)的實(shí)際作用域中才進(jìn)行同步,這樣是為了使得需要同步的操作數(shù)量盡可能變小,如果存在鎖競(jìng)爭(zhēng),那等待鎖的線程也能盡快拿到鎖。大部分情況下,上面的原則都是正確的,但是如果一系列的連續(xù)操作都對(duì)同一個(gè)對(duì)象反復(fù)加鎖和解鎖,甚至加鎖操作是出現(xiàn)在循環(huán)體中的,那即使沒有線程競(jìng)爭(zhēng),頻繁地進(jìn)行互斥同步操作也會(huì)導(dǎo)致不必要的性能損耗。
public void test() {
for (int i = 0; i < 100; i++) {
synchronized (object) {
i++;
}
}
}我們看以上方法,在for循環(huán)中,synchronized保證每個(gè)i++操作都是原子性的。但是以上的方法有個(gè)問題,就是在每次循環(huán)都會(huì)加鎖,開銷大,效率低。
虛擬機(jī)即時(shí)編譯器(JIT)在運(yùn)行時(shí),會(huì)自動(dòng)根據(jù)synchronized的影響范圍進(jìn)行鎖粗化優(yōu)化。
優(yōu)化后代碼:
public void test() {
synchronized (object) {
for (int i = 0; i < 100; i++) {
i++;
}
}
}
2. 鎖消除
消除鎖是虛擬機(jī)另外一種鎖的優(yōu)化,這種優(yōu)化更徹底,Java 虛擬機(jī)在 JIT 編譯時(shí)通過對(duì)運(yùn)行上下文的掃描,去除不可能存在共享資源競(jìng)爭(zhēng)的鎖,通過這種方式消除沒有必要的鎖,可以節(jié)省毫無意義的請(qǐng)求鎖時(shí)間,我們知道StringBuffer 是線程安全的,里面包含鎖的存在,但是如果我們?cè)诤瘮?shù)內(nèi)部使用 StringBuffer局部變量,那么代碼會(huì)在 JIT 后會(huì)自動(dòng)將鎖消除。
到此這篇關(guān)于Java并發(fā)編程中的synchronized關(guān)鍵字詳細(xì)解讀的文章就介紹到這了,更多相關(guān)Java的synchronized關(guān)鍵字內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java中使用Jedis操作Redis的實(shí)現(xiàn)代碼
本篇文章主要介紹了Java中使用Jedis操作Redis的實(shí)現(xiàn)代碼。詳細(xì)的介紹了Redis的安裝和在java中的操作,具有一定的參考價(jià)值,有興趣的可以了解一下2017-05-05
Java連接MYSQL數(shù)據(jù)庫的詳細(xì)步驟
這篇文章主要為大家介紹了Java連接MYSQL數(shù)據(jù)庫的詳細(xì)步驟,感興趣的小伙伴們可以參考一下2016-05-05
使用Jitpack發(fā)布開源Java庫的詳細(xì)流程
這篇文章主要介紹了使用Jitpack發(fā)布開源Java庫的詳細(xì)流程,本文通過圖文實(shí)例代碼相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-02-02
JAVA異常處理機(jī)制之throws/throw使用情況
這篇文章主要介紹了JAVA異常處理機(jī)制之throws/throw使用情況的區(qū)別,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
Spring事務(wù)失效的各種場(chǎng)景(13種)
本文主要介紹了Spring事務(wù)失效的各種場(chǎng)景,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07

