Java可重入鎖ReentrantLock詳解
Lock接口
在講 ReentrantLock 前,我們先熟悉一下 Lock 接口。在 Lock 接口出現(xiàn)之前, Java 程序主要是靠 synchronized 關(guān)鍵字實(shí)現(xiàn)鎖功能的,而JDK1.5之后,并發(fā)包中增加了 Lock 接口,它提供了與 synchronized 一樣的鎖功能。雖然它失去了像synchronize關(guān)鍵字隱式加鎖解鎖的便捷性,但是卻擁有了鎖獲取和釋放的可操作性,可中斷的獲取鎖以及超時(shí)獲取鎖等多種 synchronized 關(guān)鍵字所不具備的同步特性。 Lock 接口的定義如下
public interface Lock { //獲取鎖,若當(dāng)前鎖被其他線程獲取,則此線程會(huì)阻塞等待lock被釋放 //使用lock方法需要顯式地去釋放鎖,即使發(fā)生異常時(shí)也不會(huì)自動(dòng)釋放鎖。 void lock(); //作用同lock方法,并且在獲取鎖的過程中可以響應(yīng)中斷 void lockInterruptibly() throws InterruptedException; //嘗試獲取鎖,獲取成功返回true,失敗則返回false //即該方法不會(huì)導(dǎo)致線程阻塞,無論如何都會(huì)返回 boolean tryLock(); //作用同tryLock方法,如果獲取到鎖直接返回true //新增的特性是如果獲取不到鎖,會(huì)等待一段時(shí)間,且在等待的過程可以響應(yīng)中斷,一旦超過等待時(shí)間仍獲取不到鎖,就返回false boolean tryLock(long time, TimeUnit unit) throws InterruptedException; //釋放鎖 void unlock(); //返回一個(gè)綁定該lock的Condition對象,這個(gè)后續(xù)在Condition專題會(huì)詳細(xì)講解,本文先不贅述 Condition newCondition(); }
ReentrantLock 完全實(shí)現(xiàn)了Lock接口,也是JDK中唯一實(shí)現(xiàn)Lock接口的類(其余都是一些內(nèi)部類)。Lock接口的基本模板使用如下所示
//創(chuàng)建lock實(shí)例 Lock lock = new ReentrantLock(); try { //加鎖 lock.lock(); // do something... } finally { //解鎖 lock.unlock(); }
什么是ReentrantLock
ReentrantLock是一個(gè)可重入且獨(dú)占式的鎖,是一種遞歸無阻塞的同步機(jī)制。
它支持重復(fù)進(jìn)入鎖,即該鎖能夠支持一個(gè)線程對資源的重復(fù)加鎖。
除此之外,該鎖的還支持獲取鎖時(shí)的公平和非公平性選擇。
可重入概念
重進(jìn)入是指任意線程在獲取到鎖之后能夠再次獲取該鎖而不會(huì)被鎖阻塞,該特性的首先需要解決以下兩個(gè)問題:
- 線程再次獲取鎖:所需要去識別獲取鎖的線程是否為當(dāng)前占據(jù)鎖的線程,如果是,則再次獲取成功;
- 鎖的最終釋放:線程重復(fù)n次獲取了鎖,隨后在第n次釋放該鎖后,其它線程能夠獲取到該鎖。鎖的最終釋放要求鎖對于獲取進(jìn)行計(jì)數(shù)自增,計(jì)數(shù)表示當(dāng)前線程被重復(fù)獲取的次數(shù),而被釋放時(shí),計(jì)數(shù)自減,當(dāng)計(jì)數(shù)為0時(shí)表示鎖已經(jīng)成功釋放。
ReentrantLock和Synchronized對比
共同點(diǎn)
- 都可以協(xié)調(diào)多線程對共享對象、變量的訪問
- 都是可重入的,同一個(gè)線程可以多次獲得同一個(gè)鎖
- 都是阻塞式地同步,即一個(gè)線程獲取了鎖,其他訪問該鎖的線程必須阻塞在同步塊外等待
- 性能方面:Synchronized在JDK1.6的一波優(yōu)化后性能與lock差別不大
區(qū)別
- ReentrantLock需要自己手動(dòng)地獲取和釋放鎖,而synchronized關(guān)鍵字可以隱式獲得和釋放,無需用戶操心。
- ReentrantLock具有響應(yīng)中斷,超時(shí)獲取,公平非公平鎖和利用Condition綁定多個(gè)條件等特性,而synchronized不具備這些特性
- ReentrantLock是API級別的,而synchronized是JVM級別的
- ReentrantLock底層實(shí)現(xiàn)采用的是同步非阻塞,樂觀并發(fā)(CAS)策略。而synchronized是同步阻塞,悲觀并發(fā)。
類的繼承關(guān)系
類總覽:
ReentrantLock實(shí)現(xiàn)類Lock和Serializable接口。
public class ReentrantLock implements Lock, java.io.Serializable
類的內(nèi)部類
ReentrantLock 類內(nèi)部總共存在Sync、NonfairSync、FairSync三個(gè)類,其中 NonfairSync 與 FairSync 類繼承自 Sync 類, Sync 類繼承自 AbstractQueuedSynchronizer 抽象類。
Sync
Sync類繼承自AQS,它有兩個(gè)子類,分別實(shí)現(xiàn)公平鎖和非公平鎖
abstract static class Sync extends AbstractQueuedSynchronizer { //序列化版本號 private static final long serialVersionUID = -5179523762034025860L; //獲取鎖,需要子類自己實(shí)現(xiàn) abstract void lock(); //非公平方式獲取鎖 final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } //釋放鎖 protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; } //判斷資源釋放被當(dāng)前線程占用 protected final boolean isHeldExclusively() { return getExclusiveOwnerThread() == Thread.currentThread(); } //創(chuàng)建一個(gè)新條件 final ConditionObject newCondition() { return new ConditionObject(); } // 返回占有資源的線程 final Thread getOwner() { return getState() == 0 ? null : getExclusiveOwnerThread(); } //返回狀態(tài) final int getHoldCount() { return isHeldExclusively() ? getState() : 0; } //判斷資源是否被占用 final boolean isLocked() { return getState() != 0; } //自定義反序列化 private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { s.defaultReadObject(); setState(0); // reset to unlocked state } }
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)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); } //嘗試獲取鎖 protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); } }
方法細(xì)節(jié)后面詳細(xì)說明
FairSync
FairSync類也繼承了Sync類,表示采用公平策略獲取鎖,其也實(shí)現(xiàn)了Sync類中的抽象lock方法
static final class FairSync extends Sync { //序列化版本號 private static final long serialVersionUID = -3000897897090466540L; //獲取鎖 final void lock() { acquire(1); } //嘗試公平獲取鎖 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } }
方法細(xì)節(jié)后面詳細(xì)說明
類的屬性
//序列化版本號 private static final long serialVersionUID = 7373984872572414699L; //同步器,繼承自AQS,提供各種核心操作 private final Sync sync;
sync屬性非常重要,大部分的鎖操作直接轉(zhuǎn)化為該屬性的方法調(diào)用。
鎖類型
ReentrantLock 分為公平鎖和非公平鎖,分別有它的兩個(gè)內(nèi)部類 FairSync 和 NonfairSync 實(shí)現(xiàn)。
公平鎖
加鎖前檢查是否有排隊(duì)等待的線程,優(yōu)先排隊(duì)等待的線程,先來先得,即遵循FIFO原則。
非公平鎖
加鎖時(shí)不考慮排隊(duì)等待問題,直接嘗試獲取鎖,獲取不到自動(dòng)到隊(duì)尾等待
是否公平可以通過構(gòu)造方法指定。
//無參構(gòu)造函數(shù),默認(rèn)為非公平鎖 public ReentrantLock() { sync = new NonfairSync(); } //有參構(gòu)造函數(shù),傳遞boolean類型參數(shù)fair //fair=false:非公平鎖 //fair=true:公平鎖 public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
注:公平鎖為了保證時(shí)間上的絕對順序,需要頻繁的上下文切換,而非公平鎖會(huì)降低一定的上下文切換,降低性能開銷。因此, ReentrantLock 默認(rèn)選擇的是非公平鎖,則是為了減少一部分上下文切換,保證了系統(tǒng)更大的吞吐量。
獲取鎖
在講解 ReentrantLock 的源碼前,需要大家對AQS的源碼熟悉
公平鎖的獲取
我們知道,lock的獲取鎖一般使用如下方法
lock.lock();
那我們直接沿著該方法往下看,先看lock方法的實(shí)現(xiàn)
public void lock() { sync.lock(); }
調(diào)用屬性sync的lock方法,繼續(xù)深入(此時(shí)是公平鎖,所以調(diào)用的是 FairSync 對象中的 lock 方法)
final void lock() { acquire(1); }
繼續(xù)查看acquire方法
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
看到這里想必大家已經(jīng)很熟悉了,這就是AQS的acquire方法,這里就不再贅述,大家可以參考之前的AQS源碼分析文章。但是我們需要分析一下tryAcquire方法,因?yàn)樵摲椒ㄓ蠷eentrantLock中FairSync類自己實(shí)現(xiàn)的,源碼如下
protected final boolean tryAcquire(int acquires) { //獲取當(dāng)前線程 final Thread current = Thread.currentThread(); //獲取同步狀態(tài) int c = getState(); //如果同步狀態(tài)為0表示當(dāng)前資源空閑 if (c == 0) { //在CAS獲取鎖前需要調(diào)用方法hasQueuedPredecessors來判斷隊(duì)列中是否存在其他正在排隊(duì)的節(jié)點(diǎn),如果存在返回true,不僅如此if塊,否則返回false,進(jìn)行CAS操作 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { //設(shè)置當(dāng)前資源的持有線程為當(dāng)前線程 setExclusiveOwnerThread(current); return true; } } //如果c不等于0,且當(dāng)前持有鎖線程不等于當(dāng)前線程,直接返回false,加鎖失敗 //如果c不等于0,且當(dāng)前持有鎖的線程等于當(dāng)前線程,表示這是一次重入,就會(huì)把狀態(tài)+1,結(jié)束后返回true,即重入鎖返回true //這塊代碼側(cè)面說明Reentrantlock是可以重入的 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
這里需要詳細(xì)說明一下 hasQueuedPredecessors 方法(該方法的代碼也是整個(gè) Reentrantlock 代碼中的精華部分之一),需要注意一下,該方法如果返回false表示當(dāng)前線程可以直接獲取鎖,否則獲取鎖失敗。代碼如下
public final boolean hasQueuedPredecessors() { //保存尾結(jié)點(diǎn) Node t = tail; //保存頭結(jié)點(diǎn) Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }
重點(diǎn)看一下return后面的判斷
h != t &&((s = h.next) == null || s.thread != Thread.currentThread())
- 先看條件h != t
- 如果h=t,大概分為以下兩種情況
- 隊(duì)列未初始化,此時(shí)h=t=null,說明肯定沒有排隊(duì)等待的線程,可以直接獲取鎖,所以返回false
- 隊(duì)列已經(jīng)初始化 ,但是不存在排隊(duì)等待的線程。我們知道隊(duì)列在初始化的時(shí)候會(huì)新建個(gè)節(jié)點(diǎn)(thread=null)作為頭結(jié)點(diǎn),往后需要排隊(duì)的線程鏈接到這之后。因此當(dāng)隊(duì)列只有一個(gè)節(jié)點(diǎn)時(shí),滿足h=t,此時(shí)返回false,可以直接獲取鎖
- 如果h!=t,說明隊(duì)列節(jié)點(diǎn)個(gè)數(shù)大于1,即肯定存在排隊(duì)等待的線程,此時(shí)h!=t返回true,需要繼續(xù)進(jìn)行下一步判斷
- 如果h=t,大概分為以下兩種情況
- 再看條件(s = h.next) == null || s.thread != Thread.currentThread()
- 先看(s = h.next) == null:通過上面的分析我們知道隊(duì)列的節(jié)點(diǎn)個(gè)數(shù)大于1,所以s(即頭結(jié)點(diǎn)的下一節(jié)點(diǎn))肯定不為空,返回false;
- 再看s.thread != Thread.currentThread():能夠執(zhí)行到此處,整個(gè)return后面的判斷語句的結(jié)果直接取決于當(dāng)前判斷。這里判斷s節(jié)點(diǎn)對應(yīng)的線程是否是當(dāng)前線程,如果是說明此時(shí)排隊(duì)的線程就是當(dāng)前線程,即可以獲取鎖,返回false。如果不是則表示當(dāng)前還沒有輪到我(當(dāng)前線程)獲取鎖,返回true。
總結(jié)一下 hasQueuedPredecessors 方法:該方法的作用就是查看是否有排隊(duì)的線程,如果沒有直接返回false,代表當(dāng)前線程可以獲取鎖,否則再查看第一個(gè)排隊(duì)的線程是否是自己,如果是則返回false,可以獲取鎖,否則就獲取鎖失敗,進(jìn)入后續(xù)的入隊(duì)操作。
非公平鎖的獲取
非公平鎖的獲取,同樣我們來到 NonfairSync 對象中的 lock 方法
final void lock() { //直接嘗試獲取鎖 if (compareAndSetState(0, 1)) //獲取鎖成功后 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
非公平鎖的lock方法一開始就嘗試獲取CAS獲取鎖,如果CAS失敗再調(diào)用方法acquire去進(jìn)行相應(yīng)的排隊(duì)操作。
釋放鎖
公平鎖和非公平鎖的釋放流程都是一樣的
public void unlock() { sync.release(1); }
調(diào)用AQS中的release方法
public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
這里我們詳細(xì)講解一下tryRelease方法,代碼如下
protected final boolean tryRelease(int releases) { //計(jì)算釋放后state值 int c = getState() - releases; //如果獲取鎖的線程不是當(dāng)前線程,直接拋異常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //鎖被重入的次數(shù)為0,表示鎖被釋放,清空獨(dú)占線程 if (c == 0) { free = true; //清空獨(dú)占線程 setExclusiveOwnerThread(null); } //設(shè)置state值 setState(c); //返回鎖是否被釋放 return free; }
方法詳解見注釋,這里需要注意一下,調(diào)用unlock方法前需要保證鎖的占有者必須是當(dāng)前方法的調(diào)用者,否則或拋出IllegalMonitorStateException異常。
到此這篇關(guān)于Java可重入鎖ReentrantLock詳解的文章就介紹到這了,更多相關(guān)ReentrantLock詳解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
從0開始教你開發(fā)一個(gè)springboot應(yīng)用
這篇文章主要為大家介紹了從0開始開發(fā)一個(gè)springboot應(yīng)用教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-05-05Java 邏輯運(yùn)算符中&&與&,||與|的區(qū)別
這篇文章主要介紹了Java中&&與&,||與|的區(qū)別的相關(guān)資料,需要的朋友可以參考下2017-05-05SpringBoot接收數(shù)組參數(shù)和集合參數(shù)方式
這篇文章主要介紹了SpringBoot接收數(shù)組參數(shù)和集合參數(shù)方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-03-03spring cloud將spring boot服務(wù)注冊到Eureka Server上的方法
本篇文章主要介紹了spring cloud將spring boot服務(wù)注冊到Eureka Server上的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01不寫mybatis的@Param有的報(bào)錯(cuò)有的卻不報(bào)錯(cuò)問題分析
這篇文章主要為大家介紹了不寫mybatis的@Param有的報(bào)錯(cuò)有的卻不報(bào)錯(cuò)問題分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09Java數(shù)組模擬優(yōu)先級隊(duì)列數(shù)據(jù)結(jié)構(gòu)的實(shí)例
這篇文章主要介紹了Java數(shù)組模擬優(yōu)先級隊(duì)列數(shù)據(jù)結(jié)構(gòu)的實(shí)例,優(yōu)先級隊(duì)列中的元素會(huì)被設(shè)置優(yōu)先權(quán),本文的例子借助了Java中的TreeSet和TreeMap,需要的朋友可以參考下2016-04-04