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