Synchronized?和?ReentrantLock?的實(shí)現(xiàn)原理及區(qū)別
前言
在 JDK 1.5 之前共享對象的協(xié)調(diào)機(jī)制只有 synchronized 和 volatile,在 JDK 1.5 中增加了新的機(jī)制 ReentrantLock,該機(jī)制的誕生并不是為了替代 synchronized,而是在 synchronized 不適用的情況下,提供一種可以選擇的高級功能。
典型回答:
synchronized 屬于獨(dú)占式悲觀鎖,是通過 JVM 隱式實(shí)現(xiàn)的,synchronized 只允許同一時刻只有一個線程操作資源。
在 Java 中每個對象都隱式包含一個 monitor(監(jiān)視器)對象,加鎖的過程其實(shí)就是競爭 monitor 的過程,當(dāng)線程進(jìn)入字節(jié)碼 monitorenter 指令之后,線程將持有 monitor 對象,執(zhí)行 monitorexit 時釋放 monitor 對象,當(dāng)其他線程沒有拿到 monitor 對象時,則需要阻塞等待獲取該對象。
ReentrantLock 是 Lock 的默認(rèn)實(shí)現(xiàn)方式之一,它是基于 AQS(Abstract Queued Synchronizer,隊列同步器)實(shí)現(xiàn)的,它默認(rèn)是通過非公平鎖實(shí)現(xiàn)的,在它的內(nèi)部有一個 state 的狀態(tài)字段用于表示鎖是否被占用,如果是 0 則表示鎖未被占用,此時線程就可以把 state 改為 1,并成功獲得鎖,而其他未獲得鎖的線程只能去排隊等待獲取鎖資源。
synchronized 和 ReentrantLock 都提供了鎖的功能,具備互斥性和不可見性。在 JDK 1.5 中 synchronized 的性能遠(yuǎn)遠(yuǎn)低于 ReentrantLock,但在 JDK 1.6 之后 synchronized 的性能略低于 ReentrantLock,它的區(qū)別如下:
- synchronized 是 JVM 隱式實(shí)現(xiàn)的,而 ReentrantLock 是 Java 語言提供的 API;
- ReentrantLock 可設(shè)置為公平鎖,而 synchronized 卻不行;
- ReentrantLock 只能修飾代碼塊,而 synchronized 可以用于修飾方法、修飾代碼塊等;
- ReentrantLock 需要手動加鎖和釋放鎖,如果忘記釋放鎖,則會造成資源被永久占用,而 synchronized 無需手動釋放鎖;
- ReentrantLock 可以知道是否成功獲得了鎖,而 synchronized 卻不行。
考點(diǎn)分析
synchronized 和 ReentrantLock 是比線程池還要高頻的面試問題,因為它包含了更多的知識點(diǎn),且涉及到的知識點(diǎn)更加深入,對面試者的要求也更高,前面我們簡要地介紹了 synchronized 和 ReentrantLock 的概念及執(zhí)行原理,但很多大廠會更加深入的追問更多關(guān)于它們的實(shí)現(xiàn)細(xì)節(jié),比如:
- ReentrantLock 的具體實(shí)現(xiàn)細(xì)節(jié)是什么?
- JDK 1.6 時鎖做了哪些優(yōu)化?
知識擴(kuò)展
ReentrantLock 源碼分析
從源碼出發(fā)來解密 ReentrantLock 的具體實(shí)現(xiàn)細(xì)節(jié),首先來看 ReentrantLock 的兩個構(gòu)造函數(shù):
public ReentrantLock() {
sync = new NonfairSync(); // 非公平鎖
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}無參的構(gòu)造函數(shù)創(chuàng)建了一個非公平鎖,用戶也可以根據(jù)第二個構(gòu)造函數(shù),設(shè)置一個 boolean 類型的值,來決定是否使用公平鎖來實(shí)現(xiàn)線程的調(diào)度。
公平鎖 VS 非公平鎖
公平鎖的含義是線程需要按照請求的順序來獲得鎖;而非公平鎖則允許“插隊”的情況存在,所謂的“插隊”指的是,線程在發(fā)送請求的同時該鎖的狀態(tài)恰好變成了可用,那么此線程就可以跳過隊列中所有排隊的線程直接擁有該鎖。
而公平鎖由于有掛起和恢復(fù)所以存在一定的開銷,因此性能不如非公平鎖,所以 ReentrantLock 和 synchronized 默認(rèn)都是非公平鎖的實(shí)現(xiàn)方式。
ReentrantLock 是通過 lock() 來獲取鎖,并通過 unlock() 釋放鎖,使用代碼如下:
Lock lock = new ReentrantLock();
try {
// 加鎖
lock.lock();
//......業(yè)務(wù)處理
} finally {
// 釋放鎖
lock.unlock();
}ReentrantLock 中的 lock() 是通過 sync.lock() 實(shí)現(xiàn)的,但 Sync 類中的 lock() 是一個抽象方法,需要子類 NonfairSync 或 FairSync 去實(shí)現(xiàn),NonfairSync 中的 lock() 源碼如下:
final void lock() {
if (compareAndSetState(0, 1))
// 將當(dāng)前線程設(shè)置為此鎖的持有者
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}FairSync 中的 lock() 源碼如下:
final void lock() {
acquire(1);
}可以看出非公平鎖比公平鎖只是多了一行 compareAndSetState 方法,該方法是嘗試將 state 值由 0 置換為 1,如果設(shè)置成功的話,則說明當(dāng)前沒有其他線程持有該鎖,不用再去排隊了,可直接占用該鎖,否則,則需要通過 acquire 方法去排隊。
acquire 源碼如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}tryAcquire 方法嘗試獲取鎖,如果獲取鎖失敗,則把它加入到阻塞隊列中,來看 tryAcquire 的源碼:
protected?final?boolean?tryAcquire(int?acquires)?{
????final?Thread?current?=?Thread.currentThread();
????int?c?=?getState();
????if?(c?==?0)?{
????????//?公平鎖比非公平鎖多了一行代碼?!hasQueuedPredecessors()?
????????if?(!hasQueuedPredecessors()?&&
????????????compareAndSetState(0,?acquires))?{?//嘗試獲取鎖
????????????setExclusiveOwnerThread(current);?//?獲取成功,標(biāo)記被搶占
????????????return?true;
????????}
????}
????else?if?(current?==?getExclusiveOwnerThread())?{
????????int?nextc?=?c?+?acquires;
????????if?(nextc?<?0)
????????????throw?new?Error("Maximum?lock?count?exceeded");
????????setState(nextc);?//?set?state=state+1
????????return?true;
????}
????return?false;
}對于此方法來說,公平鎖比非公平鎖只多一行代碼 !hasQueuedPredecessors(),它用來查看隊列中是否有比它等待時間更久的線程,如果沒有,就嘗試一下是否能獲取到鎖,如果獲取成功,則標(biāo)記為已經(jīng)被占用。
如果獲取鎖失敗,則調(diào)用 addWaiter 方法把線程包裝成 Node 對象,同時放入到隊列中,但 addWaiter 方法并不會嘗試獲取鎖,acquireQueued 方法才會嘗試獲取鎖,如果獲取失敗,則此節(jié)點(diǎn)會被掛起,源碼如下:
/**
?*?隊列中的線程嘗試獲取鎖,失敗則會被掛起
?*/
final?boolean?acquireQueued(final?Node?node,?int?arg)?{
????boolean?failed?=?true;?//?獲取鎖是否成功的狀態(tài)標(biāo)識
????try?{
????????boolean?interrupted?=?false;?//?線程是否被中斷
????????for?(;;)?{
????????????//?獲取前一個節(jié)點(diǎn)(前驅(qū)節(jié)點(diǎn))
????????????final?Node?p?=?node.predecessor();
????????????//?當(dāng)前節(jié)點(diǎn)為頭節(jié)點(diǎn)的下一個節(jié)點(diǎn)時,有權(quán)嘗試獲取鎖
????????????if?(p?==?head?&&?tryAcquire(arg))?{
????????????????setHead(node);?//?獲取成功,將當(dāng)前節(jié)點(diǎn)設(shè)置為?head?節(jié)點(diǎn)
????????????????p.next?=?null;?//?原?head?節(jié)點(diǎn)出隊,等待被?GC
????????????????failed?=?false;?//?獲取成功
????????????????return?interrupted;
????????????}
????????????//?判斷獲取鎖失敗后是否可以掛起
????????????if?(shouldParkAfterFailedAcquire(p,?node)?&&
????????????????parkAndCheckInterrupt())
????????????????//?線程若被中斷,返回?true
????????????????interrupted?=?true;
????????}
????}?finally?{
????????if?(failed)
????????????cancelAcquire(node);
????}
}該方法會使用 for(;;) 無限循環(huán)的方式來嘗試獲取鎖,若獲取失敗,則調(diào)用 shouldParkAfterFailedAcquire 方法,嘗試掛起當(dāng)前線程,源碼如下:
/**
?*?判斷線程是否可以被掛起
?*/
private?static?boolean?shouldParkAfterFailedAcquire(Node?pred,?Node?node)?{
????//?獲得前驅(qū)節(jié)點(diǎn)的狀態(tài)
????int?ws?=?pred.waitStatus;
????//?前驅(qū)節(jié)點(diǎn)的狀態(tài)為?SIGNAL,當(dāng)前線程可以被掛起(阻塞)
????if?(ws?==?Node.SIGNAL)
????????return?true;
????if?(ws?>?0)?{?
????????do?{
????????//?若前驅(qū)節(jié)點(diǎn)狀態(tài)為?CANCELLED,那就一直往前找,直到找到一個正常等待的狀態(tài)為止
????????????node.prev?=?pred?=?pred.prev;
????????}?while?(pred.waitStatus?>?0);
????????//?并將當(dāng)前節(jié)點(diǎn)排在它后邊
????????pred.next?=?node;
????}?else?{
????????//?把前驅(qū)節(jié)點(diǎn)的狀態(tài)修改為?SIGNAL
????????compareAndSetWaitStatus(pred,?ws,?Node.SIGNAL);
????}
????return?false;
}線程入列被掛起的前提條件是,前驅(qū)節(jié)點(diǎn)的狀態(tài)為 SIGNAL,SIGNAL 狀態(tài)的含義是后繼節(jié)點(diǎn)處于等待狀態(tài),當(dāng)前節(jié)點(diǎn)釋放鎖后將會喚醒后繼節(jié)點(diǎn)。所以在上面這段代碼中,會先判斷前驅(qū)節(jié)點(diǎn)的狀態(tài),如果為 SIGNAL,則當(dāng)前線程可以被掛起并返回 true;如果前驅(qū)節(jié)點(diǎn)的狀態(tài) >0,則表示前驅(qū)節(jié)點(diǎn)取消了,這時候需要一直往前找,直到找到最近一個正常等待的前驅(qū)節(jié)點(diǎn),然后把它作為自己的前驅(qū)節(jié)點(diǎn);如果前驅(qū)節(jié)點(diǎn)正常(未取消),則修改前驅(qū)節(jié)點(diǎn)狀態(tài)為 SIGNAL。
到這里整個加鎖的流程就已經(jīng)走完了,最后的情況是,沒有拿到鎖的線程會在隊列中被掛起,直到擁有鎖的線程釋放鎖之后,才會去喚醒其他的線程去獲取鎖資源,整個運(yùn)行流程如下圖所示:

unlock 相比于 lock 來說就簡單很多了,源碼如下:
public?void?unlock()?{
????sync.release(1);
}
public?final?boolean?release(int?arg)?{
????//?嘗試釋放鎖
????if?(tryRelease(arg))?{
????????//?釋放成功
????????Node?h?=?head;
????????if?(h?!=?null?&&?h.waitStatus?!=?0)
????????????unparkSuccessor(h);
????????return?true;
????}
????return?false;
}鎖的釋放流程為,先調(diào)用 tryRelease 方法嘗試釋放鎖,如果釋放成功,則查看頭結(jié)點(diǎn)的狀態(tài)是否為 SIGNAL,如果是,則喚醒頭結(jié)點(diǎn)的下個節(jié)點(diǎn)關(guān)聯(lián)的線程;如果釋放鎖失敗,則返回 false。
tryRelease 源碼如下:
/**
?*?嘗試釋放當(dāng)前線程占有的鎖
?*/
protected?final?boolean?tryRelease(int?releases)?{
????int?c?=?getState()?-?releases;?//?釋放鎖后的狀態(tài),0?表示釋放鎖成功
????//?如果擁有鎖的線程不是當(dāng)前線程的話拋出異常
????if?(Thread.currentThread()?!=?getExclusiveOwnerThread())
????????throw?new?IllegalMonitorStateException();
????boolean?free?=?false;
????if?(c?==?0)?{?//?鎖被成功釋放
????????free?=?true;
????????setExclusiveOwnerThread(null);?//?清空獨(dú)占線程
????}
????setState(c);?//?更新?state?值,0?表示為釋放鎖成功
????return?free;
}在 tryRelease 方法中,會先判斷當(dāng)前的線程是不是占用鎖的線程,如果不是的話,則會拋出異常;如果是的話,則先計算鎖的狀態(tài)值 getState() - releases 是否為 0,如果為 0,則表示可以正常的釋放鎖,然后清空獨(dú)占的線程,最后會更新鎖的狀態(tài)并返回執(zhí)行結(jié)果。
JDK 1.6 鎖優(yōu)化
自適應(yīng)自旋鎖
JDK 1.5 在升級為 JDK 1.6 時,HotSpot 虛擬機(jī)團(tuán)隊在鎖的優(yōu)化上下了很大功夫,比如實(shí)現(xiàn)了自適應(yīng)式自旋鎖、鎖升級等。
JDK 1.6 引入了自適應(yīng)式自旋鎖意味著自旋的時間不再是固定的時間了,比如在同一個鎖對象上,如果通過自旋等待成功獲取了鎖,那么虛擬機(jī)就會認(rèn)為,它下一次很有可能也會成功 (通過自旋獲取到鎖),因此允許自旋等待的時間會相對的比較長,而當(dāng)某個鎖通過自旋很少成功獲得過鎖,那么以后在獲取該鎖時,可能會直接忽略掉自旋的過程,以避免浪費(fèi) CPU 的資源,這就是自適應(yīng)自旋鎖的功能。
鎖升級
鎖升級其實(shí)就是從偏向鎖到輕量級鎖再到重量級鎖升級的過程,這是 JDK 1.6 提供的優(yōu)化功能,也稱之為鎖膨脹。
偏向鎖是指在無競爭的情況下設(shè)置的一種鎖狀態(tài)。偏向鎖的意思是它會偏向于第一個獲取它的線程,當(dāng)鎖對象第一次被獲取到之后,會在此對象頭中設(shè)置標(biāo)示為“01”,表示偏向鎖的模式,并且在對象頭中記錄此線程的 ID,這種情況下,如果是持有偏向鎖的線程每次在進(jìn)入的話,不再進(jìn)行任何同步操作,如 Locking、Unlocking 等,直到另一個線程嘗試獲取此鎖的時候,偏向鎖模式才會結(jié)束,偏向鎖可以提高帶有同步但無競爭的程序性能。但如果在多數(shù)鎖總會被不同的線程訪問時,偏向鎖模式就比較多余了,此時可以通過 -XX:-UseBiasedLocking 來禁用偏向鎖以提高性能。
輕量鎖是相對于重量鎖而言的,在 JDK 1.6 之前,synchronized 是通過操作系統(tǒng)的互斥量(mutex lock)來實(shí)現(xiàn)的,這種實(shí)現(xiàn)方式需要在用戶態(tài)和核心態(tài)之間做轉(zhuǎn)換,有很大的性能消耗,這種傳統(tǒng)實(shí)現(xiàn)鎖的方式被稱之為重量鎖。
而輕量鎖是通過比較并交換(CAS,Compare and Swap)來實(shí)現(xiàn)的,它對比的是線程和對象的 Mark Word(對象頭中的一個區(qū)域),如果更新成功則表示當(dāng)前線程成功擁有此鎖;如果失敗,虛擬機(jī)會先檢查對象的 Mark Word 是否指向當(dāng)前線程的棧幀,如果是,則說明當(dāng)前線程已經(jīng)擁有此鎖,否則,則說明此鎖已經(jīng)被其他線程占用了。當(dāng)兩個以上的線程爭搶此鎖時,輕量級鎖就膨脹為重量級鎖,這就是鎖升級的過程,也是 JDK 1.6 鎖優(yōu)化的內(nèi)容。
總結(jié)
本文首先講了 synchronized 和 ReentrantLock 的實(shí)現(xiàn)過程,然后講了 synchronized 和 ReentrantLock 的區(qū)別,最后通過源碼的方式講了 ReentrantLock 加鎖和解鎖的執(zhí)行流程。接著又講了 JDK 1.6 中的鎖優(yōu)化,包括自適應(yīng)式自旋鎖的實(shí)現(xiàn)過程,以及 synchronized 的三種鎖狀態(tài)和鎖升級的執(zhí)行流程。
synchronized 剛開始為偏向鎖,隨著鎖競爭越來越激烈,會升級為輕量級鎖和重量級鎖。如果大多數(shù)鎖被不同的線程所爭搶就不建議使用偏向鎖了。
到此這篇關(guān)于Synchronized 和 ReentrantLock 的實(shí)現(xiàn)原理及區(qū)別的文章就介紹到這了,更多相關(guān)Synchronized 與 ReentrantLock 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java同步鎖Synchronized底層源碼和原理剖析(推薦)
- java同步鎖的正確使用方法(必看篇)
- 95%的Java程序員人都用不好Synchronized詳解
- Java?synchronized同步關(guān)鍵字工作原理
- Java synchronized偏向鎖的概念與使用
- Java?synchronized輕量級鎖實(shí)現(xiàn)過程淺析
- Java synchronized重量級鎖實(shí)現(xiàn)過程淺析
- 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必會的Synchronized底層原理剖析
- 一個例子帶你看懂Java中synchronized關(guān)鍵字到底怎么用
- 詳解Java?Synchronized的實(shí)現(xiàn)原理
- Java同步鎖synchronized用法的最全總結(jié)
相關(guān)文章
解決SpringMvc中普通類注入Service為null的問題
這篇文章主要介紹了解決SpringMvc中普通類注入Service為null的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07
spring security數(shù)據(jù)庫表結(jié)構(gòu)實(shí)例代碼
這篇文章主要介紹了spring security數(shù)據(jù)庫表結(jié)構(gòu)實(shí)例代碼,需要的朋友可以參考下2017-09-09
SpringBoot整合Redis實(shí)現(xiàn)token緩存
于token通常會被多次使用,我們需要把它保存到緩存中,以減少頻繁地訪問數(shù)據(jù)庫,本文主要介紹了SpringBoot整合Redis實(shí)現(xiàn)token緩存,感興趣的可以了解一下2024-02-02
聊聊java中一些減少if else 的編碼習(xí)慣的方法
這篇文章主要介紹了聊聊java中一些減少if else 的編碼習(xí)慣的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
RocketMQ?Broker實(shí)現(xiàn)高可用高并發(fā)的消息中轉(zhuǎn)服務(wù)
RocketMQ消息代理(Broker)是一種高可用、高并發(fā)的消息中轉(zhuǎn)服務(wù),能夠接收并存儲生產(chǎn)者發(fā)送的消息,并將消息發(fā)送給消費(fèi)者。它具有多種消息存儲模式和消息傳遞模式,支持水平擴(kuò)展和故障轉(zhuǎn)移等特性,可以為分布式應(yīng)用提供可靠的消息傳遞服務(wù)2023-04-04

