Java可重入鎖reentrantLock解析
Java可重入鎖reentrantLock
面試思考:
重入的概念是什么,實現(xiàn)原理是什么?
公平鎖和非公平鎖區(qū)別是什么,體現(xiàn)在哪些地方?
沒有競爭到鎖的線程怎么實現(xiàn)等待,釋放鎖時又是怎么被喚醒的?
我們先用reentrantLock改造一下synchronized那個程序來達到相同效果體驗一下lock:
package com.example.demo.service; import lombok.SneakyThrows; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; public class ReenterLockDemo { static int flag = 0; public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(); Condition condition = lock.newCondition(); new Thread("線程1") { @SneakyThrows public void run() { while(flag<10) { lock.lock(); try { System.out.println(Thread.currentThread().getName()+"打?。?+(++flag)); condition.signal(); condition.await(); }finally { lock.unlock(); } } }; }.start(); new Thread("線程2") { @SneakyThrows public void run() { while(flag<10) { lock.lock(); try { System.out.println(Thread.currentThread().getName()+"打印:"+(++flag)); condition.signal(); condition.await(); }finally { lock.unlock(); } } }; }.start(); } }
發(fā)現(xiàn)沒有是不是跟synchronized代碼結(jié)構(gòu)差不多,只是多了一個lock和unlock的過程。我們先畫個圖來對比一下synchronized和lock的數(shù)據(jù)結(jié)構(gòu):
這就是學技術(shù)特別有趣的地方,你看多了感覺很多高大上的技術(shù)底層設計實際都差不多。下面我們具體看下reentrantLock加鎖過程:
publicReentrantLock(){ sync =newNonfairSync(); }
ReentrantLock默認是非公平鎖。
lock方法
final voidlock(){ //先通過cas把state從0設置成1 if(compareAndSetState(0,1)) //如果設置成功說明當前線程獲取了鎖,把鎖擁有者設置成當前線程 setExclusiveOwnerThread(Thread.currentThread()); else 設置失敗則走這個方法 acquire(1); }
acquire(1);方法
public final voidacquire(int arg){ //這里if使用&&執(zhí)行了兩個方法,當兩個條件都true時會執(zhí)行selfInterrupt();,也就是中斷當前線程,這里可以猜測一下:這兩個方法應該就是獲取鎖或者進入等待隊列,當都失敗時說明加鎖過程就失敗了,直接中斷線程結(jié)束流程。 if(!tryAcquire(arg)&& acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
nonfairTryAcquire(arg)方法
final booleannonfairTryAcquire(int acquires){ //獲取當前線程 final Thread current = Thread.currentThread(); //獲取state的值,state是被volatile修飾,對所有線程都可見 int c =getState(); //當state值為0時,說明此時持有鎖的線程已經(jīng)釋放鎖,此時就可以通過cas重新競爭鎖 if(c ==0){ if(compareAndSetState(0, acquires)){ setExclusiveOwnerThread(current); returntrue; } } //當state值不為0時,判斷當前線程是否是鎖持有線程,是則把state設置成原值+acquires,獲取鎖成功 elseif(current ==getExclusiveOwnerThread()){ int nextc = c + acquires; if(nextc <0)// overflow thrownewError("Maximum lock count exceeded"); setState(nextc); returntrue; } //否則獲取鎖失敗 returnfalse; }
nonfairTryAcquire方法主要是想看看已經(jīng)獲得鎖的線程是不是當前線程,是的話可以再次獲取到鎖,體現(xiàn)了可重入的特性。不是的話就需要執(zhí)行acquireQueued方法
addWaiter(Node mode)方法
private Node addWaiter(Node mode) { //把當前線程封裝成node,nextWaiter=EXCLUSIVE= null Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; //判斷當前AQS的tail是否是null,剛開始肯定是null,但當已經(jīng)有一個node在AQS中時就不為null,不為null時把node設置為tail,pred的next指針指向node,node的prev指針指向pred if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //tail為null時執(zhí)行 enq(node); return node; }
enq(node);方法
private Node enq(final Node node) { for (;;) { Node t = tail; //當tail為null時使head=tail=new Node() if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { //不為null時把node設置為tail,pred的next指針指向node,node的prev指針指向pred,并跳出死循環(huán) node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
addWaiter方法實際就是初始化了AQS中的雙向鏈表,head和tail開始指向的是一個沒有線程的空node,之后把封裝了線程的EXCLUSIVE node放入雙向鏈表中,tail指向EXCLUSIVE node,head指向沒有線程的空node,類似于下圖:
當封裝好node并進入AQS的雙向鏈表之后進入acquireQueued方法:
acquireQueued方法
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { 獲取當前線程的前一個節(jié)點 final Node p = node.predecessor(); //如果前一個是head則會再執(zhí)行nonfairTryAcquire方法進行獲取鎖,如果獲取成功 if (p == head && tryAcquire(arg)) { 把當前線程的node設置成head,thead=null,prev=null setHead(node); 把之前的head從雙向鏈表中斷開引用,讓gc回收 p.next = null; // help GC failed = false; return interrupted; } //如果前一個節(jié)點不是head或者獲取鎖失敗則執(zhí)行if中的方法 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
shouldParkAfterFailedAcquire(p, node) 方法
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //獲取當前線程前一個node的waitStatus值,waitStatus是volatile修飾的int,所以初始化值是0 int ws = pred.waitStatus; //當waitStatus是-1時,說明當前node已經(jīng)給pred node設置過waitStatus了,直接返回true if (ws == Node.SIGNAL) return true; //當waitStatus大于0時,實際上就是1,1代表CANCELLED,就是已經(jīng)終止了的node,所以用while直接跳過所有終止的node,并返回false if (ws > 0) { do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { //如果即不等于-1也不是1,就把pred設置成-1,-1實際上就是代表當前node需要park,然后返回false compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
shouldParkAfterFailedAcquire方法總結(jié)一下就是把當前node的上一個node的waitStatus設置成-1,但waitStatus前值不能是大于0的,因為大于0代表node已經(jīng)終止了。
在設置成功后并不會執(zhí)行parkAndCheckInterrupt方法,而是再次進入了一次循環(huán)嘗試獲取鎖,再沒有成功的話就執(zhí)行parkAndCheckInterrupt進行park,然后for循環(huán)就卡住,整個流程如下:
可以看出java的lock實際就是借助LockSupport.park(this);方法來實現(xiàn),沒有獲取到鎖的線程在for循環(huán)中park住,等待持有鎖的線程來unpark喚醒他們。那么接下來看看unlock方法是怎么喚醒park線程的。
release方法
public final boolean release(int arg) { //先執(zhí)行tryRelease方法,tryRelease方法返回true繼續(xù)執(zhí)行if邏輯 if (tryRelease(arg)) { Node h = head; //head不為null并且waitStatus不為0時會執(zhí)行unparkSuccessor方法,我們知道waitStatus初始值是0,并且在shouldParkAfterFailedAcquire方法時把waitStatus設置為-1 if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
tryRelease(arg)方法
protected final boolean tryRelease(int releases) { //當前state減去releases值獲取c int c = getState() - releases; //當前線程不是擁有鎖的線程則報錯 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //如果為0則說明沒有線程持有鎖,則把擁有鎖的線程設置成null if (c == 0) { free = true; setExclusiveOwnerThread(null); } //把status設置成c setState(c); //返回是否沒有持有鎖的線程 return free; }
tryRelease方法就是在處理state變量的值,我們每次加鎖時都會增加state的值,所以在釋放鎖時就減小state的值,直到回到原始值0,說明已經(jīng)沒有線程持有鎖了。當沒有線程持有鎖時就會嘗試喚醒等待線程
unparkSuccessor(h);方法
private void unparkSuccessor(Node node) { int ws = node.waitStatus; //先拿到waitStatus,如果小于0,則把他設置成初始值0 if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; //獲取node的下一個node,如果s是null或則waitStatus大于0.則用for循環(huán)從tail往前找waitStatus小于等于0的node,直到找到最前面一個waitStatus小于等于0的node if (s == null || s.waitStatus > 0) { s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } //如果找到了則喚醒node中的線程,此時就會進入acquireQueued方法中的for循環(huán)再次嘗試獲取鎖 if (s != null) LockSupport.unpark(s.thread); }
現(xiàn)在這個非公平鎖上鎖和釋放鎖的過程都比較清晰了,我們來總結(jié)一下:lock時會先嘗試把state從0設置成1,成功則獲取鎖成功,失敗則會判斷持有鎖的線程是不是當前線程,是則獲取鎖成功,不是則會被封裝成EXCLUSIVE的node節(jié)點放入AQS的雙向鏈表等待隊列中,然后再一次判斷自己是不是第一個等待線程,是的話會再次嘗試獲取鎖,如果還是失敗則park,等待持有鎖的線程釋放鎖之后unpark喚醒。
為什么head節(jié)點是一個空節(jié)點呢
- 如果head不是空節(jié)點,在非公平鎖時p == head && tryAcquire(arg)這個判斷就有問題,因為即使當前節(jié)點的前一個節(jié)點是head,那也不能去獲取鎖,因為此時head是有線程的,head線程可能都沒執(zhí)行完或自己都是park狀態(tài),這時候獲取鎖一點意義沒有。
- 如果head不是空節(jié)點,那么在unlock時就不能用head去喚醒下一個node,此時要有更復雜的邏輯去維護各個node 狀態(tài),而且在線程被喚醒后還要維護waitStatus值,這就加大了代碼復雜性
非公平鎖相對于公平鎖來說非公平體現(xiàn)在哪里
- 在lock時非公平鎖會直接嘗試cas改變state來獲取鎖,而不是執(zhí)行tryAcquire方法
- 在執(zhí)行tryAcquire方法時非公平鎖在state是0時會直接嘗試cas改變state來獲取鎖,不用判斷AQS中有沒有線程在等待
- 在執(zhí)行acquireQueued方法執(zhí)行tryAcquire方法也會進行一次2中的過程
到此這篇關于Java可重入鎖reentrantLock解析的文章就介紹到這了,更多相關可重入鎖reentrantLock內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Spring Boot 結(jié)合 aop 實現(xiàn)讀寫分離
這篇文章主要介紹了Spring Boot 結(jié)合 aop 實現(xiàn)讀寫分離的示例,幫助大家更好的理解和使用Spring Boot框架,感興趣的朋友可以了解下2020-11-11

Intellij Idea中進行Mybatis逆向工程的實現(xiàn)

基于Java實現(xiàn)計數(shù)排序,桶排序和基數(shù)排序

SpringBoot中的Condition包下常用條件依賴注解案例介紹

Java String轉(zhuǎn)換時為null的解決方法