ReentrantLock重入鎖底層原理示例解析
J.U.C 簡(jiǎn)介
Java.util.concurrent 是在并發(fā)編程中比較常用的工具類(lèi),里面包含很多用來(lái)在并發(fā)場(chǎng)景中使用的組件。比如線程池、阻塞隊(duì)列、計(jì)時(shí)器、同步器、并發(fā)集合等等。并發(fā)包的作者是大名鼎鼎的 Doug Lea。
Lock
Lock 在 J.U.C 中是最核心的組件,鎖最重要的特性就是解決并發(fā)安全問(wèn)題。為什么要以 Lock 作為切入點(diǎn)呢?
如果你有看過(guò) J.U.C 包中的所有組件,一定會(huì)發(fā)現(xiàn)絕大部分的組件都有用到了 Lock。所以通過(guò) Lock 作為切入點(diǎn)使得在后續(xù)的學(xué)習(xí)過(guò)程中會(huì)更加輕松。
Lock 簡(jiǎn)介
在 Lock 接口出現(xiàn)之前,Java 中的應(yīng)用程序?qū)τ诙嗑€程的并發(fā)安全處理只能基于 synchronized 關(guān)鍵字來(lái)解決。但是 synchronized 在有些場(chǎng)景中會(huì)存在一些短板,也就是它并不適合于所有的并發(fā)場(chǎng)景。但是在 Java5 以后,Lock 的出現(xiàn)可以解決 synchronized 在某些場(chǎng)景中的短板,它比 synchronized 更加靈活。
Lock 的實(shí)現(xiàn)
Lock 本質(zhì)上是一個(gè)接口,它定義了釋放鎖和獲得鎖的抽象方法,定義成接口就意味著它定義了鎖的一個(gè)標(biāo)準(zhǔn)規(guī)范,也同時(shí)意味著鎖的不同實(shí)現(xiàn)。
實(shí)現(xiàn) Lock 接口的類(lèi)有很多,以下為幾個(gè)常見(jiàn)的鎖實(shí)現(xiàn)
- ReentrantLock:表示重入鎖,它是唯一一個(gè)實(shí)現(xiàn)了 Lock 接口的類(lèi)。重入鎖指的是線程在獲得鎖之后,再次獲取該鎖不需要阻塞,而是直接關(guān)聯(lián)一次計(jì)數(shù)器增加重入次數(shù)
- ReentrantReadWriteLock:重入讀寫(xiě)鎖,它實(shí)現(xiàn)了 ReadWriteLock 接口,在這個(gè)類(lèi)中維護(hù)了兩個(gè)鎖,一個(gè)是 ReadLock,一個(gè)是 WriteLock,他們都分別實(shí)現(xiàn)了 Lock 接口。讀寫(xiě)鎖是一種適合讀多寫(xiě)少的場(chǎng)景下解決線程安全問(wèn)題的工具,基本原則是: 讀和讀不互斥、讀和寫(xiě)互斥、寫(xiě)和寫(xiě)互斥。也就是說(shuō)涉及到影響數(shù)據(jù)變化的操作都會(huì)存在互斥。
- StampedLock: stampedLock 是 JDK8 引入的新的鎖機(jī)制,可以簡(jiǎn)單認(rèn)為是讀寫(xiě)鎖的一個(gè)改進(jìn)版本,讀寫(xiě)鎖雖然通過(guò)分離讀和寫(xiě)的功能使得讀和讀之間可以完全并發(fā),但是讀和寫(xiě)是有沖突的,如果大量的讀線程存在,可能會(huì)引起寫(xiě)線程的饑餓。stampedLock 是一種樂(lè)觀的讀策略,使得樂(lè)觀鎖完全不會(huì)阻塞寫(xiě)線程
Lock 的類(lèi)關(guān)系圖
Lock 有很多的鎖的實(shí)現(xiàn),但是直觀的實(shí)現(xiàn)是 ReentrantLock 重入鎖

常用API
void lock() // 如果鎖可用就獲得鎖,如果鎖不可用就阻塞直到鎖釋放 void lockInterruptibly() // 和lock()方法相似, 但阻塞的線程可中斷,拋出java.lang.InterruptedException 異常 boolean tryLock() // 非阻塞獲取鎖;嘗試獲取鎖,如果成功返回 true boolean tryLock(long timeout, TimeUnit timeUnit) //帶有超時(shí)時(shí)間的獲取鎖方法 void unlock() // 釋放鎖
ReentrantLock 重入鎖
重入鎖,表示支持重新進(jìn)入的鎖,也就是說(shuō),如果當(dāng)前線程 t1 通過(guò)調(diào)用 lock 方法獲取了鎖之后,再次調(diào)用 lock,是不會(huì)再阻塞去獲取鎖的,直接增加重試次數(shù)就行了。synchronized 和 ReentrantLock 都是可重入鎖。那為什么鎖會(huì)存在重入的特性?假如在下面這類(lèi)的場(chǎng)景中,存在多個(gè)加鎖的方法的相互調(diào)用,其實(shí)就是一種重入特性的場(chǎng)景。
重入鎖的設(shè)計(jì)目的
比如調(diào)用 demo 方法獲得了當(dāng)前的對(duì)象鎖,然后在這個(gè)方法中再去調(diào)用demo2,demo2 中的存在同一個(gè)實(shí)例鎖,這個(gè)時(shí)候當(dāng)前線程會(huì)因?yàn)闊o(wú)法獲得demo2 的對(duì)象鎖而阻塞,就會(huì)產(chǎn)生死鎖。重入鎖的設(shè)計(jì)目的是避免線程的死鎖。
public class ReentrantDemo {
public synchronized void demo() {
System.out.println("begin:demo");
demo2();
}
public void demo2() {
System.out.println("begin:demo1");
synchronized (this) {
}
}
public static void main(String[] args) {
ReentrantDemo rd = new ReentrantDemo();
new Thread(rd::demo).start();
}
}
ReentrantLock 的使用案例
public class AtomicDemo {
private static int count = 0;
static Lock lock = new ReentrantLock();
public static void inc() {
lock.lock();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
lock.unlock();
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
AtomicDemo.inc();
}).start();
;
}
Thread.sleep(3000);
System.out.println("result:" + count);
}
}
ReentrantReadWriteLock
我們以前理解的鎖,基本都是排他鎖,也就是這些鎖在同一時(shí)刻只允許一個(gè)線程進(jìn)行訪問(wèn),而讀寫(xiě)所在同一時(shí)刻可以允許多個(gè)線程訪問(wèn),但是在寫(xiě)線程訪問(wèn)時(shí),所有的讀線程和其他寫(xiě)線程都會(huì)被阻塞。讀寫(xiě)鎖維護(hù)了一對(duì)鎖,一個(gè)讀鎖、一個(gè)寫(xiě)鎖; 一般情況下,讀寫(xiě)鎖的性能都會(huì)比排它鎖好,因?yàn)榇蠖鄶?shù)場(chǎng)景讀是多于寫(xiě)的。在讀多于寫(xiě)的情況下,讀寫(xiě)鎖能夠提供比排它鎖更好的并發(fā)性和吞吐量。
public class LockDemo {
static Map<String, Object> cacheMap = new HashMap<>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock read = rwl.readLock();
static Lock write = rwl.writeLock();
public static final Object get(String key) {
System.out.println("開(kāi)始讀取數(shù)據(jù)");
read.lock(); //讀鎖
try {
return cacheMap.get(key);
} finally {
read.unlock();
}
}
public static final Object put(String key, Object value) {
write.lock();
System.out.println("開(kāi)始寫(xiě)數(shù)據(jù)");
try {
return cacheMap.put(key, value);
} finally {
write.unlock();
}
}
}
在這個(gè)案例中,通過(guò) hashmap 來(lái)模擬了一個(gè)內(nèi)存緩存,然后使用讀寫(xiě)所來(lái)保證這個(gè)內(nèi)存緩存的線程安全性。當(dāng)執(zhí)行讀操作的時(shí)候,需要獲取讀鎖,在并發(fā)訪問(wèn)的時(shí)候,讀鎖不會(huì)被阻塞,因?yàn)樽x操作不會(huì)影響執(zhí)行結(jié)果。
在執(zhí)行寫(xiě)操作是,線程必須要獲取寫(xiě)鎖,當(dāng)已經(jīng)有線程持有寫(xiě)鎖的情況下,當(dāng)前線程會(huì)被阻塞,只有當(dāng)寫(xiě)鎖釋放以后,其他讀寫(xiě)操作才能繼續(xù)執(zhí)行。使用讀寫(xiě)鎖提升讀操作的并發(fā)性,也保證每次寫(xiě)操作對(duì)所有的讀寫(xiě)操作的可見(jiàn)性。
- 讀鎖與讀鎖可以共享
- 讀鎖與寫(xiě)鎖不可以共享(排他)
- 寫(xiě)鎖與寫(xiě)鎖不可以共享(排他)
ReentrantLock 的實(shí)現(xiàn)原理
我們知道鎖的基本原理是,基于將多線程并行任務(wù)通過(guò)某一種機(jī)制實(shí)現(xiàn)線程的串行執(zhí)行,從而達(dá)到線程安全性的目的。在 synchronized 中,我們分析了偏向鎖、輕量級(jí)鎖、樂(lè)觀鎖?;跇?lè)觀鎖以及自旋鎖來(lái)優(yōu)化了 synchronized 的加鎖開(kāi)銷(xiāo),同時(shí)在重量級(jí)鎖階段,通過(guò)線程的阻塞以及喚醒來(lái)達(dá)到線程競(jìng)爭(zhēng)和同步的目的。那么在 ReentrantLock 中,也一定會(huì)存在這樣的需要去解決的問(wèn)題。就是在多線程競(jìng)爭(zhēng)重入鎖時(shí),競(jìng)爭(zhēng)失敗的線程是如何實(shí)現(xiàn)阻塞以及被喚醒的呢?
AQS 是什么
在 Lock 中,用到了一個(gè)同步隊(duì)列 AQS,全稱(chēng) AbstractQueuedSynchronizer,它是一個(gè)同步工具也是 Lock 用來(lái)實(shí)現(xiàn)線程同步的核心組件。如果你搞懂了 AQS,那么 J.U.C 中絕大部分的工具都能輕松掌握。
AQS 的兩種功能
從使用層面來(lái)說(shuō),AQS 的功能分為兩種:獨(dú)占和共享 獨(dú)占鎖,每次只能有一個(gè)線程持有鎖,比如前面給大家演示的 ReentrantLock 就是 以獨(dú)占方式實(shí)現(xiàn)的互斥鎖 共享鎖,允許多個(gè)線程同時(shí)獲取鎖,并發(fā)訪問(wèn)共享資源,比如 ReentrantReadWriteLock
AQS 的內(nèi)部實(shí)現(xiàn)
AQS 隊(duì)列內(nèi)部維護(hù)的是一個(gè) FIFO 的雙向鏈表,這種結(jié)構(gòu)的特點(diǎn)是每個(gè)數(shù)據(jù)結(jié)構(gòu)都有兩個(gè)指針,分別指向直接的后繼節(jié)點(diǎn)和直接前驅(qū)節(jié)點(diǎn)。所以雙向鏈表可以從任意一個(gè)節(jié)點(diǎn)開(kāi)始很方便的訪問(wèn)前驅(qū)和后繼。每個(gè) Node 其實(shí)是由線程封裝,當(dāng)線程爭(zhēng)搶鎖失敗后會(huì)封裝成 Node 加入到 ASQ 隊(duì)列中去;當(dāng)獲取鎖的線程釋放鎖以后,會(huì)從隊(duì)列中喚醒一個(gè)阻塞的節(jié)點(diǎn)(線程)。

Node 的組成
釋放鎖以及添加線程對(duì)于隊(duì)列的變化
當(dāng)出現(xiàn)鎖競(jìng)爭(zhēng)以及釋放鎖的時(shí)候,AQS 同步隊(duì)列中的節(jié)點(diǎn)會(huì)發(fā)生變化,首先看一下添加節(jié)點(diǎn)的場(chǎng)景。

這里會(huì)涉及到兩個(gè)變化
- 新的線程封裝成 Node 節(jié)點(diǎn)追加到同步隊(duì)列中,設(shè)置 prev 節(jié)點(diǎn)以及修改當(dāng)前節(jié)點(diǎn)的前置節(jié)點(diǎn)的 next 節(jié)點(diǎn)指向自己
- 通過(guò) CAS 講 tail 重新指向新的尾部節(jié)點(diǎn)
head 節(jié)點(diǎn)表示獲取鎖成功的節(jié)點(diǎn),當(dāng)頭結(jié)點(diǎn)在釋放同步狀態(tài)時(shí),會(huì)喚醒后繼節(jié)點(diǎn),如果后繼節(jié)點(diǎn)獲得鎖成功,會(huì)把自己設(shè)置為頭結(jié)點(diǎn),節(jié)點(diǎn)的變化過(guò)程如下

這個(gè)過(guò)程也是涉及到兩個(gè)變化
- 修改 head 節(jié)點(diǎn)指向下一個(gè)獲得鎖的節(jié)點(diǎn)
- 新的獲得鎖的節(jié)點(diǎn),將 prev 的指針指向 null
設(shè)置 head 節(jié)點(diǎn)不需要用 CAS,原因是設(shè)置 head 節(jié)點(diǎn)是由獲得鎖的線程來(lái)完成的,而同步鎖只能由一個(gè)線程獲得,所以不需要 CAS 保證,只需要把 head 節(jié)點(diǎn)設(shè)置為原首節(jié)點(diǎn)的后繼節(jié)點(diǎn),并且斷開(kāi)原 head 節(jié)點(diǎn)的 next 引用即可
ReentrantLock 的源碼分析
以 ReentrantLock 作為切入點(diǎn),來(lái)看看在這個(gè)場(chǎng)景中是如何使用 AQS 來(lái)實(shí)現(xiàn)線程的同步的
ReentrantLock 的時(shí)序圖
調(diào)用 ReentrantLock 中的 lock() 方法,源碼的調(diào)用過(guò)程我使用了時(shí)序圖來(lái)展現(xiàn)。

ReentrantLock.lock() 這個(gè)是 reentrantLock 獲取鎖的入口
public void lock() {
sync.lock();
}
sync 實(shí)際上是一個(gè)抽象的靜態(tài)內(nèi)部類(lèi),它繼承了 AQS 來(lái)實(shí)現(xiàn)重入鎖的邏輯,我們前面說(shuō)過(guò) AQS 是一個(gè)同步隊(duì)列,它能夠?qū)崿F(xiàn)線程的阻塞以及喚醒,但它并不具備業(yè)務(wù)功能,所以在不同的同步場(chǎng)景中,會(huì)繼承 AQS 來(lái)實(shí)現(xiàn)對(duì)應(yīng)場(chǎng)景的功能,Sync 有兩個(gè)具體的實(shí)現(xiàn)類(lèi),分別是:
- NofairSync:表示可以存在搶占鎖的功能,也就是說(shuō)不管當(dāng)前隊(duì)列上是否存在其他線程等待,新線程都有機(jī)會(huì)搶占鎖
- FailSync: 表示所有線程嚴(yán)格按照 FIFO 來(lái)獲取鎖
NofairSync.lock
以非公平鎖為例,來(lái)看看 lock 中的實(shí)現(xiàn)
- 非公平鎖和公平鎖最大的區(qū)別在于,在非公平鎖中我搶占鎖的邏輯是,不管有沒(méi)有線程排隊(duì),我先上來(lái) cas 去搶占一下
- CAS 成功,就表示成功獲得了鎖
- CAS 失敗,調(diào)用 acquire(1) 走鎖競(jìng)爭(zhēng)邏輯
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
CAS 的實(shí)現(xiàn)原理
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
通過(guò) cas 樂(lè)觀鎖的方式來(lái)做比較并替換,這段代碼的意思是,如果當(dāng)前內(nèi)存中的 state 的值和預(yù)期值 expect 相等,則替換為 update。更新成功返回 true,否則返回 false。
這個(gè)操作是原子的,不會(huì)出現(xiàn)線程安全問(wèn)題,這里面涉及到Unsafe這個(gè)類(lèi)的操作,以及涉及到 state 這個(gè)屬性的意義。 state 是 AQS 中的一個(gè)屬性,它在不同的實(shí)現(xiàn)中所表達(dá)的含義不一樣,對(duì)于重入鎖的實(shí)現(xiàn)來(lái)說(shuō),表示一個(gè)同步狀態(tài)。它有兩個(gè)含義的表示
- 當(dāng) state=0 時(shí),表示無(wú)鎖狀態(tài)
- 當(dāng) state>0 時(shí),表示已經(jīng)有線程獲得了鎖,也就是 state=1,但是因?yàn)镽eentrantLock 允許重入,所以同一個(gè)線程多次獲得同步鎖的時(shí)候,state 會(huì)遞增,比如重入 5 次,那么 state=5。而在釋放鎖的時(shí)候,同樣需要釋放 5 次直到 state=0其他線程才有資格獲得鎖
以上就是ReentrantLock重入鎖底層原理示例解析的詳細(xì)內(nèi)容,更多關(guān)于ReentrantLock重入鎖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java數(shù)據(jù)結(jié)構(gòu)-HashMap詳解
這篇文章主要介紹了Java數(shù)據(jù)結(jié)構(gòu)-HashMap,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03
Java人機(jī)猜拳實(shí)現(xiàn)的思路及方法實(shí)例
這篇文章主要給大家介紹了關(guān)于Java人機(jī)猜拳實(shí)現(xiàn)的思路及方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
Mybatis generator自動(dòng)生成代碼插件實(shí)例解析
這篇文章主要介紹了Mybatis generator自動(dòng)生成代碼插件實(shí)例解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09
詳解JAVA如何實(shí)現(xiàn)樂(lè)觀鎖以及CAS機(jī)制
悲觀鎖和樂(lè)觀鎖其實(shí)本質(zhì)都是一種思想,在JAVA中對(duì)于悲觀鎖的實(shí)現(xiàn)大家可能都很了解,可以通過(guò)synchronized、ReentrantLock加鎖實(shí)現(xiàn),本文不展開(kāi)講解了。那么樂(lè)觀鎖在JAVA中是如何實(shí)現(xiàn)的呢?底層的實(shí)現(xiàn)機(jī)制又是什么呢?本文就來(lái)和大家詳細(xì)講講2022-12-12
Springboot如何解決前端請(qǐng)求跨域的問(wèn)題
這篇文章主要介紹了Springboot如何解決前端請(qǐng)求跨域的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07
RabbitMQ實(shí)現(xiàn)消息可靠性傳遞過(guò)程講解
消息的可靠性傳遞是指保證消息百分百發(fā)送到消息隊(duì)列中去,這篇文章主要介紹了RabbitMQ實(shí)現(xiàn)消息可靠性傳遞過(guò)程,感興趣想要詳細(xì)了解可以參考下文2023-05-05
SpringCloud?Hystrix?斷路器的實(shí)現(xiàn)
本文主要介紹了SpringCloud?Hystrix?斷路器的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-03-03

