Synchronized?和?ReentrantLock?的實(shí)現(xiàn)原理及區(qū)別
前言
在 JDK 1.5 之前共享對(duì)象的協(xié)調(diào)機(jī)制只有 synchronized
和 volatile
,在 JDK 1.5 中增加了新的機(jī)制 ReentrantLock
,該機(jī)制的誕生并不是為了替代 synchronized
,而是在 synchronized
不適用的情況下,提供一種可以選擇的高級(jí)功能。
典型回答:
synchronized
屬于獨(dú)占式悲觀鎖,是通過 JVM 隱式實(shí)現(xiàn)的,synchronized
只允許同一時(shí)刻只有一個(gè)線程操作資源。
在 Java 中每個(gè)對(duì)象都隱式包含一個(gè) monitor(監(jiān)視器)對(duì)象,加鎖的過程其實(shí)就是競爭 monitor 的過程,當(dāng)線程進(jìn)入字節(jié)碼 monitorenter 指令之后,線程將持有 monitor 對(duì)象,執(zhí)行 monitorexit 時(shí)釋放 monitor 對(duì)象,當(dāng)其他線程沒有拿到 monitor 對(duì)象時(shí),則需要阻塞等待獲取該對(duì)象。
ReentrantLock
是 Lock 的默認(rèn)實(shí)現(xiàn)方式之一,它是基于 AQS
(Abstract Queued Synchronizer,隊(duì)列同步器)實(shí)現(xiàn)的,它默認(rèn)是通過非公平鎖實(shí)現(xiàn)的,在它的內(nèi)部有一個(gè) state
的狀態(tài)字段用于表示鎖是否被占用,如果是 0 則表示鎖未被占用,此時(shí)線程就可以把 state
改為 1,并成功獲得鎖,而其他未獲得鎖的線程只能去排隊(duì)等待獲取鎖資源。
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 需要手動(dòng)加鎖和釋放鎖,如果忘記釋放鎖,則會(huì)造成資源被永久占用,而 synchronized 無需手動(dòng)釋放鎖;
- ReentrantLock 可以知道是否成功獲得了鎖,而 synchronized 卻不行。
考點(diǎn)分析
synchronized
和 ReentrantLock
是比線程池還要高頻的面試問題,因?yàn)樗烁嗟闹R(shí)點(diǎn),且涉及到的知識(shí)點(diǎn)更加深入,對(duì)面試者的要求也更高,前面我們簡要地介紹了 synchronized
和 ReentrantLock
的概念及執(zhí)行原理,但很多大廠會(huì)更加深入的追問更多關(guān)于它們的實(shí)現(xiàn)細(xì)節(jié),比如:
- ReentrantLock 的具體實(shí)現(xiàn)細(xì)節(jié)是什么?
- JDK 1.6 時(shí)鎖做了哪些優(yōu)化?
知識(shí)擴(kuò)展
ReentrantLock 源碼分析
從源碼出發(fā)來解密 ReentrantLock
的具體實(shí)現(xiàn)細(xì)節(jié),首先來看 ReentrantLock
的兩個(gè)構(gòu)造函數(shù):
public ReentrantLock() { sync = new NonfairSync(); // 非公平鎖 } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
無參的構(gòu)造函數(shù)創(chuàng)建了一個(gè)非公平鎖,用戶也可以根據(jù)第二個(gè)構(gòu)造函數(shù),設(shè)置一個(gè) boolean 類型的值,來決定是否使用公平鎖來實(shí)現(xiàn)線程的調(diào)度。
公平鎖 VS 非公平鎖
公平鎖的含義是線程需要按照請求的順序來獲得鎖;而非公平鎖則允許“插隊(duì)”的情況存在,所謂的“插隊(duì)”指的是,線程在發(fā)送請求的同時(shí)該鎖的狀態(tài)恰好變成了可用,那么此線程就可以跳過隊(duì)列中所有排隊(duì)的線程直接擁有該鎖。
而公平鎖由于有掛起和恢復(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()
是一個(gè)抽象方法,需要子類 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)前沒有其他線程持有該鎖,不用再去排隊(duì)了,可直接占用該鎖,否則,則需要通過 acquire
方法去排隊(duì)。
acquire
源碼如下:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
tryAcquire
方法嘗試獲取鎖,如果獲取鎖失敗,則把它加入到阻塞隊(duì)列中,來看 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; }
對(duì)于此方法來說,公平鎖比非公平鎖只多一行代碼 !hasQueuedPredecessors()
,它用來查看隊(duì)列中是否有比它等待時(shí)間更久的線程,如果沒有,就嘗試一下是否能獲取到鎖,如果獲取成功,則標(biāo)記為已經(jīng)被占用。
如果獲取鎖失敗,則調(diào)用 addWaiter
方法把線程包裝成 Node 對(duì)象,同時(shí)放入到隊(duì)列中,但 addWaiter
方法并不會(huì)嘗試獲取鎖,acquireQueued
方法才會(huì)嘗試獲取鎖,如果獲取失敗,則此節(jié)點(diǎn)會(huì)被掛起,源碼如下:
/** ?*?隊(duì)列中的線程嘗試獲取鎖,失敗則會(huì)被掛起 ?*/ final?boolean?acquireQueued(final?Node?node,?int?arg)?{ ????boolean?failed?=?true;?//?獲取鎖是否成功的狀態(tài)標(biāo)識(shí) ????try?{ ????????boolean?interrupted?=?false;?//?線程是否被中斷 ????????for?(;;)?{ ????????????//?獲取前一個(gè)節(jié)點(diǎn)(前驅(qū)節(jié)點(diǎn)) ????????????final?Node?p?=?node.predecessor(); ????????????//?當(dāng)前節(jié)點(diǎn)為頭節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)時(shí),有權(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)出隊(duì),等待被?GC ????????????????failed?=?false;?//?獲取成功 ????????????????return?interrupted; ????????????} ????????????//?判斷獲取鎖失敗后是否可以掛起 ????????????if?(shouldParkAfterFailedAcquire(p,?node)?&& ????????????????parkAndCheckInterrupt()) ????????????????//?線程若被中斷,返回?true ????????????????interrupted?=?true; ????????} ????}?finally?{ ????????if?(failed) ????????????cancelAcquire(node); ????} }
該方法會(huì)使用 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,那就一直往前找,直到找到一個(gè)正常等待的狀態(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)釋放鎖后將會(huì)喚醒后繼節(jié)點(diǎn)。所以在上面這段代碼中,會(huì)先判斷前驅(qū)節(jié)點(diǎn)的狀態(tài),如果為 SIGNAL,則當(dāng)前線程可以被掛起并返回 true;如果前驅(qū)節(jié)點(diǎn)的狀態(tài) >0,則表示前驅(qū)節(jié)點(diǎn)取消了,這時(shí)候需要一直往前找,直到找到最近一個(gè)正常等待的前驅(qū)節(jié)點(diǎn),然后把它作為自己的前驅(qū)節(jié)點(diǎn);如果前驅(qū)節(jié)點(diǎn)正常(未取消),則修改前驅(qū)節(jié)點(diǎn)狀態(tài)為 SIGNAL。
到這里整個(gè)加鎖的流程就已經(jīng)走完了,最后的情況是,沒有拿到鎖的線程會(huì)在隊(duì)列中被掛起,直到擁有鎖的線程釋放鎖之后,才會(huì)去喚醒其他的線程去獲取鎖資源,整個(gè)運(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)的下個(gè)節(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
方法中,會(huì)先判斷當(dāng)前的線程是不是占用鎖的線程,如果不是的話,則會(huì)拋出異常;如果是的話,則先計(jì)算鎖的狀態(tài)值 getState() - releases
是否為 0,如果為 0,則表示可以正常的釋放鎖,然后清空獨(dú)占的線程,最后會(huì)更新鎖的狀態(tài)并返回執(zhí)行結(jié)果。
JDK 1.6 鎖優(yōu)化
自適應(yīng)自旋鎖
JDK 1.5 在升級(jí)為 JDK 1.6 時(shí),HotSpot 虛擬機(jī)團(tuán)隊(duì)在鎖的優(yōu)化上下了很大功夫,比如實(shí)現(xiàn)了自適應(yīng)式自旋鎖、鎖升級(jí)等。
JDK 1.6 引入了自適應(yīng)式自旋鎖意味著自旋的時(shí)間不再是固定的時(shí)間了,比如在同一個(gè)鎖對(duì)象上,如果通過自旋等待成功獲取了鎖,那么虛擬機(jī)就會(huì)認(rèn)為,它下一次很有可能也會(huì)成功 (通過自旋獲取到鎖),因此允許自旋等待的時(shí)間會(huì)相對(duì)的比較長,而當(dāng)某個(gè)鎖通過自旋很少成功獲得過鎖,那么以后在獲取該鎖時(shí),可能會(huì)直接忽略掉自旋的過程,以避免浪費(fèi) CPU 的資源,這就是自適應(yīng)自旋鎖的功能。
鎖升級(jí)
鎖升級(jí)其實(shí)就是從偏向鎖到輕量級(jí)鎖再到重量級(jí)鎖升級(jí)的過程,這是 JDK 1.6 提供的優(yōu)化功能,也稱之為鎖膨脹。
偏向鎖是指在無競爭的情況下設(shè)置的一種鎖狀態(tài)。偏向鎖的意思是它會(huì)偏向于第一個(gè)獲取它的線程,當(dāng)鎖對(duì)象第一次被獲取到之后,會(huì)在此對(duì)象頭中設(shè)置標(biāo)示為“01”,表示偏向鎖的模式,并且在對(duì)象頭中記錄此線程的 ID,這種情況下,如果是持有偏向鎖的線程每次在進(jìn)入的話,不再進(jìn)行任何同步操作,如 Locking
、Unlocking
等,直到另一個(gè)線程嘗試獲取此鎖的時(shí)候,偏向鎖模式才會(huì)結(jié)束,偏向鎖可以提高帶有同步但無競爭的程序性能。但如果在多數(shù)鎖總會(huì)被不同的線程訪問時(shí),偏向鎖模式就比較多余了,此時(shí)可以通過 -XX:-UseBiasedLocking
來禁用偏向鎖以提高性能。
輕量鎖是相對(duì)于重量鎖而言的,在 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)的,它對(duì)比的是線程和對(duì)象的 Mark Word(對(duì)象頭中的一個(gè)區(qū)域),如果更新成功則表示當(dāng)前線程成功擁有此鎖;如果失敗,虛擬機(jī)會(huì)先檢查對(duì)象的 Mark Word 是否指向當(dāng)前線程的棧幀,如果是,則說明當(dāng)前線程已經(jīng)擁有此鎖,否則,則說明此鎖已經(jīng)被其他線程占用了。當(dāng)兩個(gè)以上的線程爭搶此鎖時(shí),輕量級(jí)鎖就膨脹為重量級(jí)鎖,這就是鎖升級(jí)的過程,也是 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)和鎖升級(jí)的執(zhí)行流程。
synchronized
剛開始為偏向鎖,隨著鎖競爭越來越激烈,會(huì)升級(jí)為輕量級(jí)鎖和重量級(jí)鎖。如果大多數(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輕量級(jí)鎖實(shí)現(xiàn)過程淺析
- Java synchronized重量級(jí)鎖實(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必會(huì)的Synchronized底層原理剖析
- 一個(gè)例子帶你看懂Java中synchronized關(guān)鍵字到底怎么用
- 詳解Java?Synchronized的實(shí)現(xiàn)原理
- Java同步鎖synchronized用法的最全總結(jié)
相關(guān)文章
Springboot如何實(shí)現(xiàn)Web系統(tǒng)License授權(quán)認(rèn)證
這篇文章主要介紹了Springboot如何實(shí)現(xiàn)Web系統(tǒng)License授權(quán)認(rèn)證,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05Spring實(shí)戰(zhàn)之XML與JavaConfig的混合配置詳解
大家都知道Spring的顯示配置方式有兩種,一種是基于XML配置,一種是基于JavaConfig的方式配置。那么下這篇文章主要給大家分別介紹如何在JavaConfig中引用XML配置的bean以及如何在XML配置中引用JavaConfig,需要的朋友可以參考下。2017-07-07java常用工具類 XML工具類、數(shù)據(jù)驗(yàn)證工具類
這篇文章主要為大家詳細(xì)介紹了java常用工具類,包括XML工具類、數(shù)據(jù)驗(yàn)證工具類,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-05-05kafka運(yùn)維consumer-groups.sh消費(fèi)者組管理
這篇文章主要為大家介紹了kafka運(yùn)維consumer-groups.sh消費(fèi)者組管理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11Oracle + Mybatis實(shí)現(xiàn)批量插入、更新和刪除示例代碼
利用MyBatis動(dòng)態(tài)SQL的特性,我們可以做一些批量的操作,下面這篇文章主要給大家介紹了關(guān)于Oracle + Mybatis實(shí)現(xiàn)批量插入、更新和刪除的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來一起看看吧。2018-01-01MyBatis批量添加數(shù)據(jù)2種實(shí)現(xiàn)方法
這篇文章主要介紹了MyBatis批量添加數(shù)據(jù)2種實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-06-06SpringBoot 多任務(wù)并行+線程池處理的實(shí)現(xiàn)
這篇文章主要介紹了SpringBoot 多任務(wù)并行+線程池處理的實(shí)現(xiàn),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-04-04