java鎖機制ReentrantLock源碼實例分析
一:簡述
ReentrantLock是java.util.concurrent包中提供的一種鎖機制,它是一種可重入,互斥的鎖,ReentrantLock還同時支持公平和非公平兩種實現(xiàn)。本篇文章基于java8,對并發(fā)工具ReentrantLock的實現(xiàn)原理進行分析。
二:ReentrantLock類圖
三:流程簡圖
四:源碼分析
我們以lock()方法和unlock()方法為入口對ReentrantLock的源碼進行分析。
lock()源碼分析:
ReentrantLock在構造構造方法創(chuàng)建對象的時候會根據(jù)構造函數(shù)傳遞的fair參數(shù) 創(chuàng)建不同的Sync對象(默認是非公平鎖的實現(xiàn)),reentrantLock.lock()會調(diào)用sync.lock()。在Sync類中的lock()方法是一個抽象方法,具體實現(xiàn)分別在FairSync(公平)和NonfairSync(非公平)中。
public ReentrantLock() { //默認是非公平鎖 sync = new NonfairSync(); } public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
public void lock() { sync.lock(); }
非公平實現(xiàn):
final void lock() { // state表示鎖重入次數(shù) 0代表無鎖狀態(tài) //嘗試搶占鎖一次 利用cas替換state標志 替換成功代表搶占鎖成功 if (compareAndSetState(0, 1)) //搶占成功將搶占到鎖的線程設置為當前線程 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
公平鎖實現(xiàn):
final void lock() { acquire(1); }
可以看出非公平鎖在lock()方法開始會嘗試cas搶占一次鎖,也就是在這里會插隊一次。然后都會調(diào)用acquire()方法。acquire()方法中調(diào)用了三個方法,tryAcquire(),addWaiter(),acquireQueued()三個方法,而這三個方法正是ReentrantLock加鎖的核心方法,接下來我們會對這三個方法進行重點的分析。
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //這里acquireQueued()是一步步將線程是否中斷的標志傳回來的 如果為true 那么代表線程被中斷過 需要設置中斷標志 交給客戶端處理 // 因為LockSupport.park()之后不會對中斷進行響應 所以需要一步一步將中斷標記傳回來 selfInterrupt(); }
tryAcquire()方法
流程圖:
tryAcquire()方法是搶占鎖的方法,返回ture表示線程搶占到鎖了,如果搶占到鎖就什么都不做,直接執(zhí)行同步代碼,如果沒有搶占到鎖就需要將線程的信息保存起來,并且阻塞線程,也就是調(diào)用addWaiter()方法和acquireQueued()方法。
tryAcquire()也有公平和非公平鎖兩種實現(xiàn)。
公平鎖實現(xiàn):
protected final boolean tryAcquire(int acquires) { //獲取當前線程 final Thread current = Thread.currentThread(); //獲取state的值 int c = getState(); if (c == 0) { //如果state為0 代表現(xiàn)在是無鎖狀態(tài) 那么可以搶占鎖 //公平鎖這里多了一個判斷 只有在AQS鏈表中不存在元素 才去嘗試搶占鎖 否則就去鏈表中排隊 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { //cas替換成功 那么就代表搶占鎖成功了 將獲取鎖的線程設置為當前線程 setExclusiveOwnerThread(current); return true; } } //判斷獲取鎖的線程是否是當前線程 如果是的話增加重入次數(shù) (即增加state的值) else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
非公平鎖實現(xiàn):
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { //如果state為0 代表現(xiàn)在是無鎖狀態(tài) 非公平鎖這里就直接嘗試搶占鎖 // 公平鎖這里多了一個判斷 先去看AQS鏈表中有沒有已經(jīng)在等待的線程 有的話就不會嘗試去搶占鎖了,非公平鎖這里沒有這個判斷,也就是這里允許插隊(不公平) if (compareAndSetState(0, acquires)) { //cas替換成功 那么就代表搶占鎖成功了 將獲取鎖的線程設置為當前線程 setExclusiveOwnerThread(current); return true; } } //判斷獲取鎖的線程是否是當前線程 如果是的話增加重入次數(shù) (即增加state的值) else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); //重新設置state的值 setState(nextc); return true; } return false; }
接下來分析addWaiter()方法
addWaiter()
addWaiter()方法的作用就是將沒有獲取到鎖的線程封裝成一個Node對象,然后存儲在AbstractQueuedSynchronizer這個隊列同步器中(為了偷懶簡稱AQS)。我們先看一下Node對象的結(jié)構。
static final class Node { static final Node SHARED = new Node(); static final Node EXCLUSIVE = null; static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3; volatile int waitStatus; volatile Node prev; volatile Node next; volatile Thread thread; Node nextWaiter; final boolean isShared() { return nextWaiter == SHARED; } final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { // Used to establish initial head or SHARED marker } Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; } Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; } }
通過Node對象的結(jié)構我們可以看出這是一個雙向鏈表的結(jié)構,保存了prev和next引用,thread成員變量用來保存阻塞的線程引用,并且node是有狀態(tài)的,分別是CANCELLED,SIGNAL,CONDITION,PROPAGATE,其中ReentrantLock涉及到的狀態(tài)就是SIGNAL(等待被喚醒),CANCELLED(取消)(由于相關性不強,其他的狀態(tài)在后續(xù)文章用到的時候在講吧)。而AQS又保存了鏈表的頭結(jié)點head和尾結(jié)點tail,所以實際上AQS存儲阻塞線程的數(shù)據(jù)結(jié)構是一 個Node雙向鏈表。addWaiter()方法的作用就是將阻塞線程封裝成Node并且將其保存在AQS的鏈表結(jié)構中。
流程圖:
源碼分析:
private Node addWaiter(Node mode) { //將沒有獲得鎖的線程封裝成一個node Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; //如果AQS尾結(jié)點不為null 代表AQS鏈表已經(jīng)初始化 嘗試將構建好的節(jié)點添加到鏈表的尾部 if (pred != null) { node.prev = pred; //cas替換AQS的尾結(jié)點 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //沒有初始化調(diào)用enq()方法 enq(node); return node; }
private Node enq(final Node node) { //自旋 for (;;) { Node t = tail; //尾結(jié)點為空 說明AQS鏈表還沒有初始化 那么進行初始化 if (t == null) { // Must initialize //cas 將AQS的head節(jié)點 初始化 成功初始化head之后,將尾結(jié)點也初始化 //注意 這里我們可以看到head節(jié)點是不存儲線程信息的 也就是說head節(jié)點相當于是一個虛擬節(jié)點 if (compareAndSetHead(new Node())) tail = head; } else { //尾結(jié)點不為空 那么直接添加到鏈表的尾部即可 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
接下來分析acquireQueued()
acquireQueued()
acquireQueued()方法的作用就是利用Locksupport.park()方法將AQS鏈表中存儲的線程阻塞起來。
流程圖:
源碼分析:
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; //進入自旋 for (;;) { //獲取當前節(jié)點的前一個節(jié)點 final Node p = node.predecessor(); // 如果前一個節(jié)點是head 而且再次嘗試獲取鎖成功,將節(jié)點從AQS隊列中去除 并替換head 同時返回中斷標志 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; //注意 只有搶占到鎖才會跳出這個for循環(huán) return interrupted; } //去除狀態(tài)為CANCELLED的節(jié)點 并且阻塞線程 線程被阻塞在這 //注意 線程被喚醒之后繼續(xù)執(zhí)行這個for循環(huán) 嘗試搶占鎖 沒有搶占到的話又會阻塞 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) //如果失敗 將node狀態(tài)修改為CANCELLED cancelAcquire(node); } }
在acquireQueued()方法中有兩個方法比較重要shouldParkAfterFailedAcquire()方法和parkAndCheckInterrupt()方法。
shouldParkAfterFailedAcquire()
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) //如果節(jié)點是SIGNAL狀態(tài) 不需要處理 直接返回 return true; if (ws > 0) { /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ //如果節(jié)點狀態(tài)>0 說明節(jié)點是取消狀態(tài) 這種狀態(tài)的節(jié)點需要被清除 用do while循環(huán)順便清除一下前面的連續(xù)的、狀態(tài)為取消的節(jié)點 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ //正常的情況下 利用cas將前一個節(jié)點的狀態(tài)替換為 SIGNAL狀態(tài) 也就是-1 //注意 這樣隊列中節(jié)點的狀態(tài) 除了最后一個都是-1 包括head節(jié)點 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() { //掛起當前線程 并且返回中斷標志 LockSupport.park(thread) 會調(diào)用UNSAFE.park()方法將線程阻塞起來(是一個native方法) LockSupport.park(this); return Thread.interrupted(); }
到這里lock()方法也就分析完了 接下來我們分析unlock()方法
unlock()方法源碼分析:
流程圖:
reentrantLock的unlock()方法會調(diào)用sync的release()方法。
public void unlock() { //每次調(diào)用unlock 將state減1 sync.release(1); }
release()方法有兩個方法比較重要,tryRelease()方法和unparkSuccessor(),tryRelease()方法計算state的值,看線程是否已經(jīng)徹底釋放鎖(這是因為ReentrantLock是支持重入的),如果已經(jīng)徹底釋放鎖那么需要調(diào)用unparkSuccessor()方法來喚醒線程,否則不需要喚醒線程。
public final boolean release(int arg) { //只有tryRelease返回true 說明已經(jīng)釋放鎖 需要將阻塞的線程喚醒 否則不需要喚醒別的線程 if (tryRelease(arg)) { Node h = head; //如果頭結(jié)點不為空 而且狀態(tài)不為0 代表同步隊列已經(jīng)初始化 且存在需要喚醒的node //注意 同步隊列的頭結(jié)點相當于是一個虛擬節(jié)點 這一點我們可以在構建節(jié)點的代碼中很清楚的知道 //并且在shouldParkAfterFailedAcquire方法中 會把head節(jié)點的狀態(tài)修改為-1 //如果head的狀態(tài)為0 那么代表隊列中沒有需要被喚醒的元素 直接返回true if (h != null && h.waitStatus != 0) //喚醒頭結(jié)點的下一個節(jié)點 unparkSuccessor(h); return true; } return false; }
tryRelease()
protected final boolean tryRelease(int releases) { //減少重入次數(shù) int c = getState() - releases; //如果獲取鎖的線程不是當前線程 拋出異常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //如果state為0 說明已經(jīng)徹底釋放了鎖 返回true if (c == 0) { free = true; //將獲取鎖的線程設置為null setExclusiveOwnerThread(null); } //重置state的值 setState(c); //如果釋放了鎖 就返回true 否則返回false return free; }
unparkSuccessor()
private void unparkSuccessor(Node node) { //獲取頭結(jié)點的狀態(tài) 將頭結(jié)點狀態(tài)設置為0 代表現(xiàn)在正在有線程被喚醒 如果head狀態(tài)為0 就不會進入這個方法了 int ws = node.waitStatus; if (ws < 0) //將頭結(jié)點狀態(tài)設置為0 compareAndSetWaitStatus(node, ws, 0); /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ //喚醒頭結(jié)點的下一個狀態(tài)不是cancelled的節(jié)點 (因為頭結(jié)點是不存儲阻塞線程的) Node s = node.next; //當前節(jié)點是null 或者是cancelled狀態(tài) if (s == null || s.waitStatus > 0) { s = null; //從aqs鏈表的尾部開始遍歷 找到離頭結(jié)點最近的 不為空的 狀態(tài)不是cancelled的節(jié)點 賦值給s 這里為什么從尾結(jié)點開始遍歷而是頭結(jié)點 應該是因為添加結(jié)點的時候是先初始化結(jié)點的prev的, 從尾結(jié)點開始遍歷 不會出現(xiàn)prve沒有賦值的情況 for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) //調(diào)用LockSupport.unpark()喚醒指定的線程 LockSupport.unpark(s.thread); }
五:總結(jié)
最后總結(jié)一下,有以下幾點需要注意:
1.ReentrantLock有公平鎖和非公平鎖兩種實現(xiàn),其實這兩種實現(xiàn)的差別只有兩點,第一是在lock()方法開始的時候非公平鎖會嘗試cas搶占鎖插一次隊, 第二是在tryAcquire()方法發(fā)現(xiàn)state為0的時候,非公平鎖會搶占一次鎖,而公平鎖會判斷AQS鏈表中是否存在等待的線程,沒有等待的線程才會去搶占鎖。
2.AQS存儲阻塞線程的數(shù)據(jù)結(jié)構是一個雙向鏈表的結(jié)構,而且它是遵循先進先出的,因為它是從頭結(jié)點的下一個節(jié)點開始喚醒,而添加新的節(jié)點的時候是添加到鏈表的尾部,所以AQS同時也是一個隊列的數(shù)據(jù)結(jié)構。
3.線程被喚醒之后會繼續(xù)執(zhí)行acquireQueued()方法,因為它是阻塞在acquireQueued()方法的for循環(huán)中的,喚醒后嘗試去獲取鎖,獲取成功就會將節(jié)點從AQS中刪除并跳出for循環(huán),否則又會繼續(xù)阻塞。(獲取鎖失敗的原因就是因為有人插隊啊。。也就是非公平鎖導致的)。
以上就是java鎖機制ReentrantLock源碼實例分析的詳細內(nèi)容,更多關于java鎖機制ReentrantLock的資料請關注腳本之家其它相關文章!
相關文章
Intellij IDEA導入JAVA項目并啟動(圖文教程)
這篇文章主要介紹了Intellij IDEA導入JAVA項目并啟動,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-08-08解決idea找不到類could not find artifact問題
本文總結(jié)了解決Java項目中找不到類的問題的常見解決方案,包括刷新Maven項目、清理IDEA緩存、Maven Clean Install、重新Package、解決依賴沖突和手動導入依賴包等方法2025-01-01SpringBoot實現(xiàn)多環(huán)境配置文件切換教程詳解
很多時候,我們項目在開發(fā)環(huán)境和生成環(huán)境的環(huán)境配置是不一樣的,例如,數(shù)據(jù)庫配置,這個時候就需要切換環(huán)境配置文件。本文將詳細講解SpringBoot如何切換配置文件,需要的可以參考一下2022-03-03利用Java的MyBatis框架獲取MySQL中插入記錄時的自增主鍵
這篇文章主要介紹了利用Java的MyBatis框架獲取MySQL中插入記錄的自增長字段值,其中大家可以看到MyBatis支持普通SQL語句所帶來的遍歷,需要的朋友可以參考下2016-06-06Windows下apache ant安裝、環(huán)境變量配置教程
這篇文章主要介紹了Windows下apache ant安裝、環(huán)境變量配置教程,ANT的安裝很簡單,本文同時講解了驗證安裝是否成功的方法和使用方法實例,需要的朋友可以參考下2015-06-06詳解Java 網(wǎng)絡IO編程總結(jié)(BIO、NIO、AIO均含完整實例代碼)
本篇文章主要介紹了Java 網(wǎng)絡IO編程總結(jié)(BIO、NIO、AIO均含完整實例代碼),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-12-12