ReentrantLock重入鎖底層原理示例解析
J.U.C 簡介
Java.util.concurrent 是在并發(fā)編程中比較常用的工具類,里面包含很多用來在并發(fā)場景中使用的組件。比如線程池、阻塞隊(duì)列、計(jì)時(shí)器、同步器、并發(fā)集合等等。并發(fā)包的作者是大名鼎鼎的 Doug Lea。
Lock
Lock 在 J.U.C 中是最核心的組件,鎖最重要的特性就是解決并發(fā)安全問題。為什么要以 Lock 作為切入點(diǎn)呢?
如果你有看過 J.U.C 包中的所有組件,一定會(huì)發(fā)現(xiàn)絕大部分的組件都有用到了 Lock。所以通過 Lock 作為切入點(diǎn)使得在后續(xù)的學(xué)習(xí)過程中會(huì)更加輕松。
Lock 簡介
在 Lock 接口出現(xiàn)之前,Java 中的應(yīng)用程序?qū)τ诙嗑€程的并發(fā)安全處理只能基于 synchronized 關(guān)鍵字來解決。但是 synchronized 在有些場景中會(huì)存在一些短板,也就是它并不適合于所有的并發(fā)場景。但是在 Java5 以后,Lock 的出現(xiàn)可以解決 synchronized 在某些場景中的短板,它比 synchronized 更加靈活。
Lock 的實(shí)現(xiàn)
Lock 本質(zhì)上是一個(gè)接口,它定義了釋放鎖和獲得鎖的抽象方法,定義成接口就意味著它定義了鎖的一個(gè)標(biāo)準(zhǔn)規(guī)范,也同時(shí)意味著鎖的不同實(shí)現(xiàn)。
實(shí)現(xiàn) Lock 接口的類有很多,以下為幾個(gè)常見的鎖實(shí)現(xiàn)
- ReentrantLock:表示重入鎖,它是唯一一個(gè)實(shí)現(xiàn)了 Lock 接口的類。重入鎖指的是線程在獲得鎖之后,再次獲取該鎖不需要阻塞,而是直接關(guān)聯(lián)一次計(jì)數(shù)器增加重入次數(shù)
- ReentrantReadWriteLock:重入讀寫鎖,它實(shí)現(xiàn)了 ReadWriteLock 接口,在這個(gè)類中維護(hù)了兩個(gè)鎖,一個(gè)是 ReadLock,一個(gè)是 WriteLock,他們都分別實(shí)現(xiàn)了 Lock 接口。讀寫鎖是一種適合讀多寫少的場景下解決線程安全問題的工具,基本原則是: 讀和讀不互斥、讀和寫互斥、寫和寫互斥。也就是說涉及到影響數(shù)據(jù)變化的操作都會(huì)存在互斥。
- StampedLock: stampedLock 是 JDK8 引入的新的鎖機(jī)制,可以簡單認(rèn)為是讀寫鎖的一個(gè)改進(jìn)版本,讀寫鎖雖然通過分離讀和寫的功能使得讀和讀之間可以完全并發(fā),但是讀和寫是有沖突的,如果大量的讀線程存在,可能會(huì)引起寫線程的饑餓。stampedLock 是一種樂觀的讀策略,使得樂觀鎖完全不會(huì)阻塞寫線程
Lock 的類關(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)入的鎖,也就是說,如果當(dāng)前線程 t1 通過調(diào)用 lock 方法獲取了鎖之后,再次調(diào)用 lock,是不會(huì)再阻塞去獲取鎖的,直接增加重試次數(shù)就行了。synchronized 和 ReentrantLock 都是可重入鎖。那為什么鎖會(huì)存在重入的特性?假如在下面這類的場景中,存在多個(gè)加鎖的方法的相互調(diào)用,其實(shí)就是一種重入特性的場景。
重入鎖的設(shè)計(jì)目的
比如調(diào)用 demo 方法獲得了當(dāng)前的對(duì)象鎖,然后在這個(gè)方法中再去調(diào)用demo2,demo2 中的存在同一個(gè)實(shí)例鎖,這個(gè)時(shí)候當(dāng)前線程會(huì)因?yàn)闊o法獲得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)行訪問,而讀寫所在同一時(shí)刻可以允許多個(gè)線程訪問,但是在寫線程訪問時(shí),所有的讀線程和其他寫線程都會(huì)被阻塞。讀寫鎖維護(hù)了一對(duì)鎖,一個(gè)讀鎖、一個(gè)寫鎖; 一般情況下,讀寫鎖的性能都會(huì)比排它鎖好,因?yàn)榇蠖鄶?shù)場景讀是多于寫的。在讀多于寫的情況下,讀寫鎖能夠提供比排它鎖更好的并發(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("開始讀取數(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("開始寫數(shù)據(jù)"); try { return cacheMap.put(key, value); } finally { write.unlock(); } } }
在這個(gè)案例中,通過 hashmap 來模擬了一個(gè)內(nèi)存緩存,然后使用讀寫所來保證這個(gè)內(nèi)存緩存的線程安全性。當(dāng)執(zhí)行讀操作的時(shí)候,需要獲取讀鎖,在并發(fā)訪問的時(shí)候,讀鎖不會(huì)被阻塞,因?yàn)樽x操作不會(huì)影響執(zhí)行結(jié)果。
在執(zhí)行寫操作是,線程必須要獲取寫鎖,當(dāng)已經(jīng)有線程持有寫鎖的情況下,當(dāng)前線程會(huì)被阻塞,只有當(dāng)寫鎖釋放以后,其他讀寫操作才能繼續(xù)執(zhí)行。使用讀寫鎖提升讀操作的并發(fā)性,也保證每次寫操作對(duì)所有的讀寫操作的可見性。
- 讀鎖與讀鎖可以共享
- 讀鎖與寫鎖不可以共享(排他)
- 寫鎖與寫鎖不可以共享(排他)
ReentrantLock 的實(shí)現(xiàn)原理
我們知道鎖的基本原理是,基于將多線程并行任務(wù)通過某一種機(jī)制實(shí)現(xiàn)線程的串行執(zhí)行,從而達(dá)到線程安全性的目的。在 synchronized 中,我們分析了偏向鎖、輕量級(jí)鎖、樂觀鎖?;跇酚^鎖以及自旋鎖來優(yōu)化了 synchronized 的加鎖開銷,同時(shí)在重量級(jí)鎖階段,通過線程的阻塞以及喚醒來達(dá)到線程競爭和同步的目的。那么在 ReentrantLock 中,也一定會(huì)存在這樣的需要去解決的問題。就是在多線程競爭重入鎖時(shí),競爭失敗的線程是如何實(shí)現(xiàn)阻塞以及被喚醒的呢?
AQS 是什么
在 Lock 中,用到了一個(gè)同步隊(duì)列 AQS,全稱 AbstractQueuedSynchronizer,它是一個(gè)同步工具也是 Lock 用來實(shí)現(xiàn)線程同步的核心組件。如果你搞懂了 AQS,那么 J.U.C 中絕大部分的工具都能輕松掌握。
AQS 的兩種功能
從使用層面來說,AQS 的功能分為兩種:獨(dú)占和共享 獨(dú)占鎖,每次只能有一個(gè)線程持有鎖,比如前面給大家演示的 ReentrantLock 就是 以獨(dú)占方式實(shí)現(xiàn)的互斥鎖 共享鎖,允許多個(gè)線程同時(shí)獲取鎖,并發(fā)訪問共享資源,比如 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)開始很方便的訪問前驅(qū)和后繼。每個(gè) Node 其實(shí)是由線程封裝,當(dāng)線程爭搶鎖失敗后會(huì)封裝成 Node 加入到 ASQ 隊(duì)列中去;當(dāng)獲取鎖的線程釋放鎖以后,會(huì)從隊(duì)列中喚醒一個(gè)阻塞的節(jié)點(diǎn)(線程)。
Node 的組成
釋放鎖以及添加線程對(duì)于隊(duì)列的變化
當(dāng)出現(xiàn)鎖競爭以及釋放鎖的時(shí)候,AQS 同步隊(duì)列中的節(jié)點(diǎn)會(huì)發(fā)生變化,首先看一下添加節(jié)點(diǎn)的場景。
這里會(huì)涉及到兩個(gè)變化
- 新的線程封裝成 Node 節(jié)點(diǎn)追加到同步隊(duì)列中,設(shè)置 prev 節(jié)點(diǎn)以及修改當(dāng)前節(jié)點(diǎn)的前置節(jié)點(diǎn)的 next 節(jié)點(diǎn)指向自己
- 通過 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)的變化過程如下
這個(gè)過程也是涉及到兩個(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)是由獲得鎖的線程來完成的,而同步鎖只能由一個(gè)線程獲得,所以不需要 CAS 保證,只需要把 head 節(jié)點(diǎn)設(shè)置為原首節(jié)點(diǎn)的后繼節(jié)點(diǎn),并且斷開原 head 節(jié)點(diǎn)的 next 引用即可
ReentrantLock 的源碼分析
以 ReentrantLock 作為切入點(diǎn),來看看在這個(gè)場景中是如何使用 AQS 來實(shí)現(xiàn)線程的同步的
ReentrantLock 的時(shí)序圖
調(diào)用 ReentrantLock 中的 lock() 方法,源碼的調(diào)用過程我使用了時(shí)序圖來展現(xiàn)。
ReentrantLock.lock() 這個(gè)是 reentrantLock 獲取鎖的入口
public void lock() { sync.lock(); }
sync 實(shí)際上是一個(gè)抽象的靜態(tài)內(nèi)部類,它繼承了 AQS 來實(shí)現(xiàn)重入鎖的邏輯,我們前面說過 AQS 是一個(gè)同步隊(duì)列,它能夠?qū)崿F(xiàn)線程的阻塞以及喚醒,但它并不具備業(yè)務(wù)功能,所以在不同的同步場景中,會(huì)繼承 AQS 來實(shí)現(xiàn)對(duì)應(yīng)場景的功能,Sync 有兩個(gè)具體的實(shí)現(xiàn)類,分別是:
- NofairSync:表示可以存在搶占鎖的功能,也就是說不管當(dāng)前隊(duì)列上是否存在其他線程等待,新線程都有機(jī)會(huì)搶占鎖
- FailSync: 表示所有線程嚴(yán)格按照 FIFO 來獲取鎖
NofairSync.lock
以非公平鎖為例,來看看 lock 中的實(shí)現(xiàn)
- 非公平鎖和公平鎖最大的區(qū)別在于,在非公平鎖中我搶占鎖的邏輯是,不管有沒有線程排隊(duì),我先上來 cas 去搶占一下
- CAS 成功,就表示成功獲得了鎖
- CAS 失敗,調(diào)用 acquire(1) 走鎖競爭邏輯
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); }
通過 cas 樂觀鎖的方式來做比較并替換,這段代碼的意思是,如果當(dāng)前內(nèi)存中的 state 的值和預(yù)期值 expect 相等,則替換為 update。更新成功返回 true,否則返回 false。
這個(gè)操作是原子的,不會(huì)出現(xiàn)線程安全問題,這里面涉及到Unsafe這個(gè)類的操作,以及涉及到 state 這個(gè)屬性的意義。 state 是 AQS 中的一個(gè)屬性,它在不同的實(shí)現(xiàn)中所表達(dá)的含義不一樣,對(duì)于重入鎖的實(shí)現(xiàn)來說,表示一個(gè)同步狀態(tài)。它有兩個(gè)含義的表示
- 當(dāng) state=0 時(shí),表示無鎖狀態(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)文章
SpringBoot結(jié)合Redis哨兵模式的實(shí)現(xiàn)示例
這篇文章主要介紹了SpringBoot結(jié)合Redis哨兵模式的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04使用Spring的AbstractRoutingDataSource實(shí)現(xiàn)多數(shù)據(jù)源切換示例
這篇文章主要介紹了使用Spring的AbstractRoutingDataSource實(shí)現(xiàn)多數(shù)據(jù)源切換示例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-02-02java發(fā)送短信系列之限制日發(fā)送次數(shù)
這篇文章主要為大家詳細(xì)介紹了java發(fā)送短信系列之限制日發(fā)送次數(shù),詳細(xì)介紹了限制每日向同一個(gè)用戶(根據(jù)手機(jī)號(hào)和ip判斷)發(fā)送短信次數(shù)的方法,感興趣的小伙伴們可以參考一下2016-02-02mybatis中resultMap 標(biāo)簽的使用教程
resultMap 標(biāo)簽用來描述如何從數(shù)據(jù)庫結(jié)果集中來加載對(duì)象,這篇文章重點(diǎn)給大家介紹mybatis中resultMap 標(biāo)簽的使用,感興趣的朋友一起看看吧2018-07-07java中使用@Transactional會(huì)有哪些坑
在Java中,@Transactional是一個(gè)常用的注解,用于聲明方法應(yīng)該在一個(gè)事務(wù)的上下文中執(zhí)行,本文主要介紹了java中使用@Transactional會(huì)有哪些坑,感興趣的可以了解一下2024-04-04SpringBoot如何啟動(dòng)自動(dòng)加載自定義模塊yml文件(PropertySourceFactory)
這篇文章主要介紹了SpringBoot如何啟動(dòng)自動(dòng)加載自定義模塊yml文件(PropertySourceFactory),具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07Spring Boot的Maven插件Spring Boot Maven plu
Spring Boot的Maven插件Spring Boot Maven plugin以Maven的方式提供Spring Boot支持,Spring Boot Maven plugin將Spring Boot應(yīng)用打包為可執(zhí)行的jar或war文件,然后以通常的方式運(yùn)行Spring Boot應(yīng)用,本文介紹Spring Boot的Maven插件Spring Boot Maven plugin,一起看看吧2024-01-01