詳解Java?ReentrantLock可重入,可打斷,鎖超時的實(shí)現(xiàn)原理
概述
前面講解了ReentrantLock加鎖和解鎖的原理實(shí)現(xiàn),但是沒有闡述它的可重入、可打斷以及超時獲取鎖失敗的原理,本文就重點(diǎn)講解這三種情況。建議大家先看下這篇文章了解下ReentrantLock加鎖的基本原理,圖解Java ReentrantLock公平鎖和非公平鎖的實(shí)現(xiàn)。
可重入
可重入是指一個線程如果獲取了鎖,那么它就是鎖的主人,那么它可以再次獲取這把鎖,這種就是理解為重入,簡而言之,可以重復(fù)獲取同一把鎖,不會造成阻塞,舉個例子如下:
@Test public void testRepeatLock() { ReentrantLock reentrantLock = new ReentrantLock(); // 第一次獲取鎖 reentrantLock.lock(); try { System.out.println(Thread.currentThread().getName() + " first get lock"); // 再次獲取鎖 tryAgainLock(reentrantLock); }finally { reentrantLock.unlock(); } } public void tryAgainLock(ReentrantLock reentrantLock) { // 第2次獲取鎖 reentrantLock.lock(); try { System.out.println(Thread.currentThread().getName() + " second get lock"); }finally { reentrantLock.unlock(); } }
- 同一個線程使用ReentrantLock多次獲取鎖,不會阻塞
- 申請幾把鎖,最后需要解除幾把鎖
那你知道是怎么實(shí)現(xiàn)的嗎?
概述的文章中已經(jīng)講解了ReentrantLock整個的加鎖和解鎖的過程,可重入實(shí)現(xiàn)就在其中,這里著重關(guān)注下申請鎖的方法tryAcquire,最終會調(diào)用nonfairTryAcquire方法。
- 如果已經(jīng)有線程獲得了鎖, 并且占用鎖的線程是當(dāng)前線程, 表示【發(fā)生了鎖重入】,上圖的1步驟
- 計(jì)算出沖入的次數(shù)nextc等于當(dāng)前次數(shù)+新增次數(shù),acquires等于1
- 更新 state 的值,這里不使用 cas 是因?yàn)楫?dāng)前線程正在持有鎖,所以這里的操作相當(dāng)于在一個管程內(nèi), 然后返回ture,表明再次申請鎖成功。
可打斷
ReentrantLock相比于synchronized加鎖一大優(yōu)勢是可打斷,那么什么是可打斷呢?ReentrantLock通過lockInterruptibly()
加鎖,如果一直獲取不到鎖,可以通過調(diào)用線程的interrupt()
提前終止線程。舉個例子:
@Test public void testInterrupt() throws InterruptedException { ReentrantLock lock = new ReentrantLock(); // 主線程普通加鎖 System.out.println("主線程優(yōu)先獲取鎖"); lock.lock(); try { // 創(chuàng)建子線程 Thread t1 = new Thread(() -> { try { System.out.println("t1嘗試獲取打斷鎖"); lock.lockInterruptibly(); } catch (InterruptedException e) { System.out.println("t1沒有獲取到鎖,被打斷,直接返回"); return; } try { System.out.println("t1成功獲取鎖"); } finally { System.out.println("t1釋放鎖"); lock.unlock(); } }, "t1"); t1.start(); Thread.sleep(2000); System.out.println("主線程進(jìn)行打斷鎖"); t1.interrupt(); } finally { // 主線程解鎖 System.out.println("主線程優(yōu)先釋放鎖"); lock.unlock(); } }
- 通過
lockInterruptibly()
方法獲取鎖期間,可以通過線程的interrupt()
方法進(jìn)行中斷,跳出阻塞。 - 通過
lock()
方法獲取鎖,不會響應(yīng)interrupt()
方法的中斷。
接下來我們看看它的實(shí)現(xiàn)原理。
public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } public final void acquireInterruptibly(int arg) { // 被其他線程打斷了直接返回 false if (Thread.interrupted()) throw new InterruptedException(); if (!tryAcquire(arg)) // 沒獲取到鎖,進(jìn)入這里 doAcquireInterruptibly(arg); }
- 先判斷一次線程是否中斷了,是的話,直接拋出中斷異常。
- 如果沒有獲取鎖,調(diào)用
doAcquireInterruptibly()
方法。
private void doAcquireInterruptibly(int arg) throws InterruptedException { // 封裝當(dāng)前線程,加入到隊(duì)列中 final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { // 自旋 for (;;) { // shouldParkAfterFailedAcquire判斷是否需要阻塞等待 // parkAndCheckInterrupt方法是阻塞線程,返回true,表示線程被中斷了 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 【在 park 過程中如果被 interrupt 會拋出異?!? 而不會再次進(jìn)入循環(huán)獲取鎖后才完成打斷效果 throw new InterruptedException(); } } finally { // 拋出異常前會進(jìn)入這里 if (failed) // 取消當(dāng)前線程的節(jié)點(diǎn) cancelAcquire(node); } }
addWaiter
將當(dāng)前線程封裝成節(jié)點(diǎn),加入到隊(duì)列中。shouldParkAfterFailedAcquire()
方法判斷如果前一個節(jié)點(diǎn)的等待狀態(tài)時-1,則返回true,表示當(dāng)前線程需要阻塞。parkAndCheckInterrupt()
方法是阻塞線程,返回true,表示線程被中斷了,拋出InterruptedException
異常。- 最后調(diào)用
cancelAcquire()
方法,將當(dāng)前節(jié)點(diǎn)狀態(tài)設(shè)置為cancel取消狀態(tài)。
// 取消節(jié)點(diǎn)出隊(duì)的邏輯 private void cancelAcquire(Node node) { // 判空 if (node == null) return; // 把當(dāng)前節(jié)點(diǎn)封裝的 Thread 置為空 node.thread = null; // 獲取當(dāng)前取消的 node 的前驅(qū)節(jié)點(diǎn) Node pred = node.prev; // 前驅(qū)節(jié)點(diǎn)也被取消了,循環(huán)找到前面最近的沒被取消的節(jié)點(diǎn) while (pred.waitStatus > 0) node.prev = pred = pred.prev; // 獲取前驅(qū)節(jié)點(diǎn)的后繼節(jié)點(diǎn),可能是當(dāng)前 node,也可能是 waitStatus > 0 的節(jié)點(diǎn) Node predNext = pred.next; // 把當(dāng)前節(jié)點(diǎn)的狀態(tài)設(shè)置為 【取消狀態(tài) 1】 node.waitStatus = Node.CANCELLED; // 條件成立說明當(dāng)前節(jié)點(diǎn)是尾節(jié)點(diǎn),把當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)設(shè)置為尾節(jié)點(diǎn) if (node == tail && compareAndSetTail(node, pred)) { // 把前驅(qū)節(jié)點(diǎn)的后繼節(jié)點(diǎn)置空,這里直接把所有的取消節(jié)點(diǎn)出隊(duì) compareAndSetNext(pred, predNext, null); } else { // 說明當(dāng)前節(jié)點(diǎn)不是 tail 節(jié)點(diǎn) int ws; // 條件一成立說明當(dāng)前節(jié)點(diǎn)不是 head.next 節(jié)點(diǎn) if (pred != head && // 判斷前驅(qū)節(jié)點(diǎn)的狀態(tài)是不是 -1,不成立說明前驅(qū)狀態(tài)可能是 0 或者剛被其他線程取消排隊(duì)了 ((ws = pred.waitStatus) == Node.SIGNAL || // 如果狀態(tài)不是 -1,設(shè)置前驅(qū)節(jié)點(diǎn)的狀態(tài)為 -1 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && // 前驅(qū)節(jié)點(diǎn)的線程不為null pred.thread != null) { Node next = node.next; // 當(dāng)前節(jié)點(diǎn)的后繼節(jié)點(diǎn)是正常節(jié)點(diǎn) if (next != null && next.waitStatus <= 0) // 把 前驅(qū)節(jié)點(diǎn)的后繼節(jié)點(diǎn) 設(shè)置為 當(dāng)前節(jié)點(diǎn)的后繼節(jié)點(diǎn),【從隊(duì)列中刪除了當(dāng)前節(jié)點(diǎn)】 compareAndSetNext(pred, predNext, next); } else { // 當(dāng)前節(jié)點(diǎn)是 head.next 節(jié)點(diǎn),喚醒當(dāng)前節(jié)點(diǎn)的后繼節(jié)點(diǎn) unparkSuccessor(node); } node.next = node; // help GC } }
鎖超時
ReentrantLock還具備鎖超時的能力,調(diào)用tryLock(long timeout, TimeUnit unit)方法,在給定時間內(nèi)獲取鎖,獲取不到就退出,這也是synchronized沒有的功能。
@Test public void testLockTimeout() throws InterruptedException { ReentrantLock lock = new ReentrantLock(); Thread t1 = new Thread(() -> { try { // 調(diào)用tryLock獲取鎖 if (!lock.tryLock(2, TimeUnit.SECONDS)) { System.out.println("t1獲取不到鎖"); return; } } catch (InterruptedException e) { System.out.println("t1被打斷,獲取不到鎖"); return; } try { System.out.println("t1獲取到鎖"); } finally { lock.unlock(); } }, "t1"); // 主線程加鎖 lock.lock(); System.out.println("主線程獲取到鎖"); t1.start(); Thread.sleep(3000); try { System.out.println("主線程釋放了鎖"); } finally { lock.unlock(); } }
那這個原理實(shí)現(xiàn)是什么樣的呢?
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { // 調(diào)用tryAcquireNanos方法 return sync.tryAcquireNanos(1, unit.toNanos(timeout)); } public final boolean tryAcquireNanos(int arg, long nanosTimeout) { if (Thread.interrupted()) throw new InterruptedException(); // tryAcquire 嘗試一次,獲取不到的話調(diào)用doAcquireNanos方法 return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }
private boolean doAcquireNanos(int arg, long nanosTimeout) { if (nanosTimeout <= 0L) return false; // 獲取最后期限的時間戳 final long deadline = System.nanoTime() + nanosTimeout; // 將當(dāng)前線程添加到隊(duì)列中 final Node node = addWaiter(Node.EXCLUSIVE); boolean failed = true; try { // 自旋 for (;;) { // 獲取前驅(qū)節(jié)點(diǎn) final Node p = node.predecessor(); // 前驅(qū)節(jié)點(diǎn)是head,嘗試獲取鎖 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return true; } // 計(jì)算還需等待的時間 nanosTimeout = deadline - System.nanoTime(); if (nanosTimeout <= 0L) //時間已到 return false; if (shouldParkAfterFailedAcquire(p, node) && // 如果 nanosTimeout 大于該值,才有阻塞的意義,否則直接自旋會好點(diǎn) nanosTimeout > spinForTimeoutThreshold) LockSupport.parkNanos(this, nanosTimeout); // 【被打斷會報(bào)異常】 if (Thread.interrupted()) throw new InterruptedException(); } } }
- 如果nanosTimeout小于0,表示到了指定時間沒有獲取鎖成功,返回false
- 如果 nanosTimeout 大于spinForTimeoutThreshold,值為1000L,進(jìn)行阻塞。因?yàn)闀r間太短阻塞沒有意義,否則直接自旋會好點(diǎn)。
到此這篇關(guān)于詳解Java ReentrantLock可重入,可打斷,鎖超時的實(shí)現(xiàn)原理的文章就介紹到這了,更多相關(guān)Java ReentrantLock內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring Boot與Redisson實(shí)時排行榜功能
排行榜功能是常見且重要的需求之一,本文主要介紹了Spring Boot與Redisson實(shí)時排行榜功能,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-05-05關(guān)于SpringBoot的@ConfigurationProperties注解和松散綁定、數(shù)據(jù)校驗(yàn)
這篇文章主要介紹了關(guān)于SpringBoot的@ConfigurationProperties注解和松散綁定、數(shù)據(jù)校驗(yàn),@ConfigurationProperties主要作用就是將prefix屬性指定的前綴配置項(xiàng)的值綁定到這個JavaBean上?,通過指定的前綴,來綁定配置文件中的配置,需要的朋友可以參考下2023-05-05如何在java文件中設(shè)置文字顏色:setTextColor()
這篇文章主要介紹了如何在java文件中設(shè)置文字顏色:setTextColor(),文末補(bǔ)充介紹了在java代碼中設(shè)置字體顏色方法總結(jié),結(jié)合實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-09-09Java爬蟲范例之使用Htmlunit爬取學(xué)校教務(wù)網(wǎng)課程表信息
htmlunit 是一款開源的java 頁面分析工具,讀取頁面后,可以有效的使用htmlunit分析頁面上的內(nèi)容。項(xiàng)目可以模擬瀏覽器運(yùn)行,被譽(yù)為java瀏覽器的開源實(shí)現(xiàn)。今天我們用這款分析工具來爬取學(xué)校教務(wù)網(wǎng)課程表信息2021-11-11