Java中的ReentrantLock原理解析
一、示例分析
公平鎖
/*** *說明: 該示例使用的是公平策略。 */ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class MyThread extends Thread { private Lock lock; public MyThread(String name, Lock lock) { super(name); this.lock = lock; } public void run () { lock.lock(); try { System.out.println(Thread.currentThread() + " running"); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } } finally { lock.unlock(); } } } public class AbstractQueuedSynchonizerDemo { public static void main(String[] args) throws InterruptedException { Lock lock = new ReentrantLock(true); MyThread t1 = new MyThread("t1", lock); MyThread t2 = new MyThread("t2", lock); MyThread t3 = new MyThread("t3", lock); t1.start(); t2.start(); t3.start(); } } //運(yùn)行結(jié)果: Thread[t1,5,main] running Thread[t2,5,main] running Thread[t3,5,main] running
二、加鎖lock過程
- 獲取當(dāng)前線程
- 獲取鎖的狀態(tài)getState()
- 判斷鎖的狀態(tài)
- 如果鎖是自由狀態(tài)則第五步,如果不是自由狀態(tài)則第七步
- 判斷自己是否需要排隊
- 什么情況下當(dāng)前線程不需要排隊(排隊=入隊+阻塞)
- 隊列沒有初始 對頭和隊尾等于null的時候是不需要排隊
- 隊列當(dāng)中只有一個線程的時候是不需要排隊的
- 如果不需要排隊則cas加鎖
- 判斷是否重入(一般情況下不重入)
- 下不重入直接返回false(加鎖失?。?/li>
- 加鎖失敗之后會調(diào)用addWaiter,主要是入隊(入隊不等于排隊)入隊完成之后第一個節(jié)點(diǎn)是一個虛擬出來的節(jié)點(diǎn)(thread等于null),即前置節(jié)點(diǎn),而不是我們?nèi)腙牭墓?jié)點(diǎn)
- 判斷是否需要自旋
- 如果需要自旋則再次獲取鎖,如果失敗則park
- 如果不需要自旋則直接park
線程執(zhí)行l(wèi)ock.lock,下圖給出了方法調(diào)用中的主要方法。
由上圖可知,最后的結(jié)果是t3線程會被禁止,因為調(diào)用了LockSupport.park。
三、加鎖總結(jié)
- AQS框架 第一個線程t1獲取鎖的時候 代價基本為0(cas鎖的狀態(tài),記錄當(dāng)前持有鎖的線程),而且連隊列都為null(隊列都沒有初始化)
- 當(dāng)?shù)谝粋€線程釋放鎖之后第二個線程t2來加鎖(t1 和t2是沒有競爭執(zhí)行),代價基本為0,(cas鎖的狀態(tài),記錄當(dāng)前持有鎖的線程),而且連隊列都為null(隊列都沒有初始化),因為是重復(fù)上面步驟6
- t1沒有釋放鎖這個t2來加鎖,鎖的情況如下
- t2會加鎖失敗,則返回false,然后調(diào)用addWaiter方法區(qū)初始化隊列,然后自己入隊
- 會把t2封裝成為一個node,調(diào)用ENQ方法讓當(dāng)前node入隊
- 入隊成功之后調(diào)用acquireQueued final Node p = node.predecessor();獲取上一個節(jié)點(diǎn)
- 判斷上一個節(jié)點(diǎn)是否頭節(jié)點(diǎn)
- 如果是頭結(jié)點(diǎn)則再次獲取鎖(一次自旋)
- 如果獲取到了鎖,表示t1釋放了鎖(AQS框架當(dāng)中第一個node永遠(yuǎn)可以理解為是當(dāng)前持有鎖的線程)
- 如果t1沒有釋放鎖,t2自旋一次之后還是沒有獲取到鎖則park 排隊
- t3來加鎖,t1沒有釋放鎖,t2這個時候已經(jīng)排隊(和t2的區(qū)別在于t3入隊之后不會自旋,直接排 隊)。
- t3 因為t1沒有釋放鎖,所以t3肯定拿不到鎖,肯定會調(diào)用addWaiter–enq方法入隊。
四、解鎖unlock
第一種情況:
- 只有一個t1上鎖了,當(dāng)調(diào)用unlock解鎖的時候,sync.release(1);
- 把鎖的狀態(tài)改為自由狀態(tài)
- boolean free = false; 只是標(biāo)識一下目前還沒有釋放鎖成功,因為你僅僅把鎖改成了自由狀態(tài),線程沒有釋放(而且還有可能是重入),所以這個變量是一個過渡變量。
- setExclusiveOwnerThread(null) 把持有鎖的線程改為null 鎖徹底釋放了
- 以上是釋放鎖,接下來可能需要喚醒隊列當(dāng)中的阻塞線程去獲取鎖(因為有可能不需要喚醒)
- Node h = head;拿到隊頭if (h != null && h.waitStatus != 0)判斷是否有對頭,是否有線程在排隊
- 由于當(dāng)前這種情況肯定沒有人排隊則不需要喚醒,則return true 標(biāo)識解鎖成功
第二種情況:
就是隊列當(dāng)中有線程排隊,比如t2
- 調(diào)用tryRelease方法釋放
- if (h != null && h.waitStatus != 0) 判斷是否需要喚醒下一個,當(dāng)前這種情況肯定需要喚醒一個
- Node h = head;把頭結(jié)點(diǎn)傳給了unparkSuccessor,unparkSuccessor(h);
- 得到隊列當(dāng)中第一個排隊的線程, 也就是t2所標(biāo)識的node對象LockSupport.unpark(s.thread); 喚醒t2線程
- 由于t2在lock方法中被阻塞那么喚醒則也是從lock方法中被喚醒往下執(zhí)行
private final boolean parkAndCheckInterrupt() { //簡單的喚醒t2 LockSupport.park(this); //這里為什么需要調(diào)用一下Thread.interrupted() return Thread.interrupted(); }
Thread.interrupted()這個方法主要干嘛?清除打斷標(biāo)記(復(fù)位)
五、內(nèi)部類
Sync類存在如下方法和作用如下。
NonfairSync類 NonfairSync類繼承了Sync類,表示采用非公平策略獲取鎖,其實(shí)現(xiàn)了Sync類中抽象的lock方法,源碼如下:
// 非公平鎖 static final class NonfairSync extends Sync { // 版本號 private static final long serialVersionUID = 7316153563782823691L; // 獲得鎖 final void lock() { if (compareAndSetState(0, 1)) // 比較并設(shè)置狀態(tài)成功,狀態(tài)0表示鎖沒有被占用 // 把當(dāng)前線程設(shè)置獨(dú)占了鎖 setExclusiveOwnerThread(Thread.currentThread()); else // 鎖已經(jīng)被占用,或者set失敗 // 以獨(dú)占模式獲取對象,忽略中斷 acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
從lock方法的源碼可知,每一次都嘗試獲取鎖,而并不會按照公平等待的原則進(jìn)行等待,讓等待時間最久的線程獲得鎖。
FairSyn類 FairSync類也繼承了Sync類,表示采用公平策略獲取鎖,其實(shí)現(xiàn)了Sync類中的抽象lock方法,源碼如下:
// 公平鎖 static final class FairSync extends Sync { // 版本序列化 private static final long serialVersionUID = -3000897897090466540L; final void lock() { // 以獨(dú)占模式獲取對象,忽略中斷 acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ // 嘗試公平獲取鎖 protected final boolean tryAcquire(int acquires) { // 獲取當(dāng)前線程 final Thread current = Thread.currentThread(); // 獲取狀態(tài) int c = getState(); if (c == 0) { // 狀態(tài)為0 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { // 不存在已經(jīng)等待更久的線程并且比較并且設(shè)置狀態(tài)成功 // 設(shè)置當(dāng)前線程獨(dú)占 setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 狀態(tài)不為0,即資源已經(jīng)被線程占據(jù) // 下一個狀態(tài) int nextc = c + acquires; if (nextc < 0) // 超過了int的表示范圍 throw new Error("Maximum lock count exceeded"); // 設(shè)置狀態(tài) setState(nextc); return true; } return false; } }
跟蹤lock方法的源碼可知,當(dāng)資源空閑時,它總是會先判斷sync隊列(AbstractQueuedSynchronizer中的數(shù)據(jù)結(jié)構(gòu))是否有等待時間更長的線程,如果存在,則將該線程加入到等待隊列的尾部,實(shí)現(xiàn)了公平獲取原則。其中,F(xiàn)airSync類的lock的方法調(diào)用如下,只給出了主要的方法。
可以看出只要資源被其他線程占用,該線程就會添加到sync queue中的尾部,而不會先嘗試獲取資源。這也是和Nonfair最大的區(qū)別,Nonfair每一次都會嘗試去獲取資源,如果此時該資源恰好被釋放,則會被當(dāng)前線程獲取,這就造成了不公平的現(xiàn)象,當(dāng)獲取不成功,再加入隊列尾部。
六、類的構(gòu)造函數(shù)
ReentrantLock()型構(gòu)造函數(shù);默認(rèn)是采用的非公平策略獲取鎖
public ReentrantLock() { // 默認(rèn)非公平策略 sync = new NonfairSync(); }
ReentrantLock(boolean)型構(gòu)造函數(shù)
可以傳遞參數(shù)確定采用公平策略或者是非公平策略,參數(shù)為true表示公平策略,否則,采用非公平策略:
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
核心函數(shù)分析
通過分析ReentrantLock的源碼,可知對其操作都轉(zhuǎn)化為對Sync對象的操作,由于Sync繼承了AQS,所以基本上都可以轉(zhuǎn)化為對AQS的操作。如將ReentrantLock的lock函數(shù)轉(zhuǎn)化為對Sync的lock函數(shù)的調(diào)用,而具體會根據(jù)采用的策略(如公平策略或者非公平策略)的不同而調(diào)用到Sync的不同子類。 所以可知,在ReentrantLock的背后,是AQS對其服務(wù)提供了支持。
到此這篇關(guān)于Java中的ReentrantLock原理解析的文章就介紹到這了,更多相關(guān)ReentrantLock原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決SpringCloud Feign傳對象參數(shù)調(diào)用失敗的問題
這篇文章主要介紹了解決SpringCloud Feign傳對象參數(shù)調(diào)用失敗的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-06-06SpringBoot特點(diǎn)之依賴管理和自動裝配(實(shí)例代碼)
在使用SpringBoot的時候,會自動將Bean裝配到IoC容器中,操作也很簡單,今天小編給大家介紹下SpringBoot特點(diǎn)之依賴管理和自動裝配的知識,感興趣的朋友一起看看吧2022-03-03Spring Boot整合RabbitMQ實(shí)例(Topic模式)
Topic Exchange 轉(zhuǎn)發(fā)消息主要是根據(jù)通配符。接下來通過本文給大家分享Spring Boot整合RabbitMQ實(shí)例(Topic模式),需要的朋友參考下吧2017-04-04Springboot jar包 idea 遠(yuǎn)程調(diào)試的操作過程
文章介紹了如何在IntelliJ IDEA中遠(yuǎn)程調(diào)試Spring Boot項目的Jar包,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-11-11Activiti7通過代碼動態(tài)生成工作流實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了Activiti7通過代碼動態(tài)生成工作流實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11JAVA面試題之Forward與Redirect的區(qū)別詳解
這篇文章主要給大家介紹了在JAVA面試中可能遇到會遇到的一道題,就是java中Forward與Redirect兩者之前的區(qū)別,文中介紹的非常詳細(xì),對大家具有一定參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-05-05詳解Spring中@Valid和@Validated注解用法
本文將以新增一個員工為功能切入點(diǎn),以常規(guī)寫法為背景,慢慢烘托出?@Valid?和?@Validated?注解用法詳解,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2022-07-07