ReentrantLock是Java并發(fā)包中提供的一個可重入的互斥鎖揪惦,它擁有與synchronized相同的作用遍搞,但卻比synchronized有更好的性能,在許多高并發(fā)編程中都會用到它器腋。由于大部分同學(xué)都只停留在了API調(diào)用的層次溪猿,對ReentrantLock的原理一知半解,甚至一無所知纫塌,因此寫下了這篇文章诊县,讓同學(xué)們真正的把ReentrantLock給拿下!
本文將會從以下幾個方面去進(jìn)行分享:
- 使用場景
- 源碼實(shí)現(xiàn)
- 設(shè)計思想
使用場景
public class ReentrantLockTest {
private ReentrantLock lock = new ReentrantLock();
public void method() {
lock.lock();
// do something
lock.unlock();
}
}
ReentrantLock的使用十分簡單措左,在同步代碼塊前調(diào)用lock()加鎖依痊,同步代碼塊之后調(diào)用unlock()釋放鎖就可以了。另外要注意怎披,lock()和unlock()必須成雙成對的出現(xiàn)胸嘁。如果同步代碼塊可能拋出異常,則必須把unlock()調(diào)用放在finally塊里钳枕。
源碼實(shí)現(xiàn)
打開lock()查看它的實(shí)現(xiàn)缴渊。
public void lock() {
sync.lock();
}
它通過調(diào)用了sync的lock()方法來完成加鎖,我們?nèi)タ聪聅ync的定義鱼炒。
private final ReentrantLock.Sync sync;
Sync是一個內(nèi)部類衔沼,我們?nèi)タ聪滤膌ock()實(shí)現(xiàn)。
abstract void lock();
很明顯昔瞧,有子類繼承了Sync指蚁,這時候我們可以去看sync的初始化代碼,看看是使用了哪個子類對sync進(jìn)行了初始化自晰。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
有兩個子類用于sync的初始化凝化,F(xiàn)airSync和NonfairSync。這其實(shí)就是我們所熟知的公平鎖和非公平鎖酬荞。ReentrantLock默認(rèn)情況下使用了非公平鎖搓劫,當(dāng)然也可以在創(chuàng)建ReentrantLock的時候顯示指定瞧哟。
現(xiàn)在我們先去看下非公平鎖NonfairSync對lock()的實(shí)現(xiàn)。
final void lock() {
// 這步快速嘗試獲取鎖的操作枪向,公平鎖里邊沒有
if (this.compareAndSetState(0, 1)) {
this.setExclusiveOwnerThread(Thread.currentThread());
} else {
this.acquire(1);
}
}
查看代碼可知勤揩,非公平鎖一上來就先調(diào)用一把compareAndSetState(),嘗試獲取鎖秘蛔,這個對于已經(jīng)在鎖隊列里苦苦等待的其他線程陨亡,是非常不公平的。劃重點(diǎn)了深员,同學(xué)們负蠕,這里是公平鎖和非公平鎖的重要區(qū)別。
現(xiàn)在我們來看compareAndSetState()的實(shí)現(xiàn)倦畅。
protected final boolean compareAndSetState(int var1, int var2) {
return unsafe.compareAndSwapInt(this, stateOffset, var1, var2);
}
原理是通過unsafe提供的CAS原子操作進(jìn)行state的值更新遮糖。另外發(fā)現(xiàn)compareAndSetState()是位于AbstractQueuedSynchronizer類中的,繼而發(fā)現(xiàn)滔迈,Sync繼承了AbstractQueuedSynchronizer止吁,我們需要更新的state也位于AbstractQueuedSynchronizer中被辑。
private volatile int state;
state記錄了鎖重入的次數(shù)燎悍,如果為0,那么表示當(dāng)前沒有線程持有此鎖盼理,此時使用一個CAS操作即可快速完成鎖的申請谈山,這便是快速嘗試。
當(dāng)快速嘗試失敗之后宏怔,將會調(diào)用acquire()方法奏路,acquire()也是來自于AbstractQueuedSynchronizer,我們看下代碼臊诊。
public final void acquire(int var1) {
if (!this.tryAcquire(var1) && this.acquireQueued(this.addWaiter(Node.EXCLUSIVE), var1)) {
selfInterrupt();
}
}
先tryAcquire()一下鸽粉,萬一自己是個鎖二代(鎖重入)呢,那就爽歪歪了抓艳,獲取鎖成功触机,直接撤退走人!
來看下非公平鎖是怎么獲取鎖的玷或,打開tryAcquire()的源碼儡首。
protected final boolean tryAcquire(int var1) {
return this.nonfairTryAcquire(var1);
}
繼續(xù)查看Sync中的nonfairTryAcquire()。
final boolean nonfairTryAcquire(int var1) {
// 獲取當(dāng)前線程
Thread var2 = Thread.currentThread();
// 獲取鎖重人次數(shù)
int var3 = this.getState();
// lock還沒有被任何線程霸占偏友,趕緊快速嘗試一把加鎖
if (var3 == 0) {
if (this.compareAndSetState(0, var1)) {
this.setExclusiveOwnerThread(var2);
return true;
}
}
// lock已經(jīng)被線程霸占了蔬胯,檢查一下是不是自己人,如果是的話位他,那當(dāng)前線程就是鎖二代了氛濒,state加1
else if (var2 == this.getExclusiveOwnerThread()) {
int var4 = var3 + var1;
// 鎖重入的次數(shù)不能超過Ingteger.MAX_VALUE产场,不然會爆炸
if (var4 < 0) {
throw new Error("Maximum lock count exceeded");
}
this.setState(var4);
return true;
}
// 既沒有創(chuàng)業(yè)成功,也不是鎖二代舞竿,就只有失敗的命運(yùn)了
return false;
}
看完nonfairTryAcquire()的操作涝动,我們知道非公平鎖的鎖重入是怎么玩的了。
如果線程沒有獲取到鎖炬灭,就只能去隊列里等待鎖了醋粟,也就是調(diào)用addWaiter()方法,我們來看下它的實(shí)現(xiàn)重归。
private Node addWaiter(Node mode) {
// 構(gòu)造一個Node米愿,與當(dāng)前線程綁定,mode傳入的是Node.EXCLUSIVE鼻吮,代表獨(dú)占鎖
Node var2 = new Node(Thread.currentThread(), mode);
// 獲取隊列末尾節(jié)點(diǎn)
Node var3 = this.tail;
// 末尾節(jié)點(diǎn)非空育苟,CAS快速嘗試,把自己更新為末尾節(jié)點(diǎn)
if (var3 != null) {
var2.prev = var3;
if (this.compareAndSetTail(var3, var2)) {
var3.next = var2;
return var2;
}
}
// 末尾節(jié)點(diǎn)不存在椎木,或者更新末尾節(jié)點(diǎn)失敗了
this.enq(var2);
return var2;
}
當(dāng)末尾節(jié)點(diǎn)為null违柏,或者更新末尾節(jié)點(diǎn)失敗了,那就調(diào)用enq()進(jìn)行處理香椎。
private Node enq(Node var1) {
// 注意這里的while(true)漱竖,不達(dá)目的不罷休
while(true) {
Node var2 = this.tail;
// 末尾節(jié)點(diǎn)為空,意味著整個隊列都為空畜伐,頭節(jié)點(diǎn)自然不存在馍惹,那就來初始化一波頭尾節(jié)點(diǎn)
if (var2 == null) {
// 通過CAS更新頭節(jié)點(diǎn),從這行代碼我們也可以知道玛界,鎖隊列里的頭節(jié)點(diǎn)是空的万矾,沒有和任何線程綁定
if (this.compareAndSetHead(new Node())) {
// 此時頭節(jié)點(diǎn)和末尾節(jié)點(diǎn)是同一個
this.tail = this.head;
}
}
// 末尾節(jié)點(diǎn)已經(jīng)存在,直接CAS把自己更新為末尾節(jié)點(diǎn)
else {
var1.prev = var2;
if (this.compareAndSetTail(var2, var1)) {
var2.next = var1;
return var2;
}
}
}
}
用上了while(true)慎框,保證了enq()返回后良狈,當(dāng)前線程一定是被加入到了鎖隊列的末尾。
當(dāng)前線程對應(yīng)的Node加入隊列末尾之后笨枯,接著調(diào)用了acquireQueued()薪丁,我們來看下這個方法干了什么事。
final boolean acquireQueued(Node var1, int var2) {
// 標(biāo)識該方法返回時猎醇,當(dāng)前線程是否已獲得鎖窥突,默認(rèn)值true代表沒有搶到
boolean var3 = true;
try {
// 標(biāo)識一下當(dāng)前線程在睡覺時候有沒有被叫醒過
boolean var4 = false;
// 自旋獲取鎖
while(true) {
// 獲取當(dāng)前節(jié)點(diǎn)的上一個節(jié)點(diǎn)
Node var5 = var1.predecessor();
// 如果上一個節(jié)點(diǎn)是頭節(jié)點(diǎn)的話,就可以直接嘗試搶鎖
if (var5 == this.head && this.tryAcquire(var2)) {
//把自己設(shè)置為頭節(jié)點(diǎn)
this.setHead(var1);
// 解除上一任頭節(jié)點(diǎn)的依賴硫嘶,讓它早日被GC干掉
var5.next = null;
// 標(biāo)識我已經(jīng)搶鎖成功啦
var3 = false;
// 最終的返回值阻问,居然是當(dāng)前線程睡覺時候有沒有被叫醒過
boolean var6 = var4;
return var6;
}
// 當(dāng)前節(jié)點(diǎn)不在頭節(jié)點(diǎn)之后,或者在頭節(jié)點(diǎn)之后沦疾,但是搶鎖失敗了
// 調(diào)用shouldParkAfterFailedAcquire()称近,為自己找到一個歸宿(讓上一個節(jié)點(diǎn)完事之后通知自己)第队,然后就可以調(diào)用parkAndCheckInterrupt()讓自己去休眠了
if (shouldParkAfterFailedAcquire(var5, var1) && this.parkAndCheckInterrupt()) {
// 睡覺時被意外喚醒,記錄一下刨秆,自己也是發(fā)生過中斷的男人了
var4 = true;
}
}
} finally {
// 如果var3為true凳谦,則證明線程沒有拿到鎖,并且它已經(jīng)廢了衡未,所以方法退出前尸执,得調(diào)用cancelAcquire()給線程收尸
if (var3) {
this.cancelAcquire(var1);
}
}
}
線程被加入到隊列之后,就是瘋狂自旋的干上面這幾件事情:找人叫醒自己缓醋,睡覺如失,被叫醒,周而復(fù)始送粱,直到自己拿到了鎖褪贵,然后離開。
至于線程怎么找人叫醒自己的抗俄,我們來看shouldParkAfterFailedAcquire()的實(shí)現(xiàn)脆丁。
// var0是上一個節(jié)點(diǎn),var1是當(dāng)前節(jié)點(diǎn)
private static boolean shouldParkAfterFailedAcquire(Node var0, Node var1) {
int var2 = var0.waitStatus;
// 上一個節(jié)點(diǎn)滿足被叫醒的條件动雹,那也就意味著上一個節(jié)點(diǎn)早晚會搶鎖槽卫,用完鎖后自然會通知自己,這樣的話洽胶,自己就可以安心去睡覺了
if (var2 == -1) {
return true;
} else {
// 上一個節(jié)點(diǎn)放棄搶鎖啦晒夹,指望不上了,繼續(xù)往前尋找可靠的節(jié)點(diǎn)作為依靠
if (var2 > 0) {
do {
var1.prev = var0 = var0.prev;
} while(var0.waitStatus > 0);
var0.next = var1;
} else { // waitStatus不大于0姊氓,CAS把它設(shè)置為-1(滿足被喚醒的條件),但是設(shè)置不一定會成功
compareAndSetWaitStatus(var0, var2, -1);
}
// 這一波操作喷好,沒有找到喚醒自己的人翔横,睡不成啰
return false;
}
}
既然睡不成,那還是繼續(xù)去看看有沒有搶鎖資格吧梗搅,有就搶一把禾唁,就這樣周而復(fù)始的的循環(huán)下去。
當(dāng)某一時刻无切,線程找到了能叫醒自己的人荡短,這時候它就可以去睡覺了,去睡覺自然就是調(diào)用parkAndCheckInterrupt()方法哆键。
private final boolean parkAndCheckInterrupt() {
// 劃重點(diǎn)了掘托,同學(xué)們,線程阻塞就是調(diào)用這個API來完成的籍嘹,底層的實(shí)現(xiàn)是用的unsafe.park()
LockSupport.park(this);
// 線程睡醒了闪盔,但是它要判斷一下睡覺期間有沒有發(fā)生過中斷
return Thread.interrupted();
}
如果發(fā)生過中斷弯院,則parkAndCheckInterrupt()會返回true。這是我們再去看acquire()方法泪掀,它會執(zhí)行selfInterrupt()听绳。
static void selfInterrupt() {
// 給線程標(biāo)記上中斷位,這可謂中斷會延遲處理异赫,但是從未缺席
Thread.currentThread().interrupt();
}
同學(xué)們椅挣,到這里lock()就分析完了。現(xiàn)在我們接著來看看unlock()是怎么玩的塔拳。
public void unlock() {
this.sync.release(1);
}
看樣子是調(diào)用了AQS的release()方法贴妻,我們接著看。
public final boolean release(int var1) {
// 釋放鎖蝙斜,只有state變?yōu)?了才會返回true
if (this.tryRelease(var1)) {
Node var2 = this.head;
// 頭節(jié)點(diǎn)不為空名惩,代表隊列不為空,waitStatus不為0孕荠,代表它有后繼節(jié)點(diǎn)娩鹉,因此可以去喚醒下家去搶鎖
if (var2 != null && var2.waitStatus != 0) {
this.unparkSuccessor(var2);
}
return true;
} else {
return false;
}
}
調(diào)用tryRelease()方法釋放鎖,看下它的實(shí)現(xiàn)稚伍。
protected final boolean tryRelease(int var1) {
// state減1后的結(jié)果
int var2 = this.getState() - var1;
// 如果線程不是當(dāng)前鎖的線程弯予,那就玩大啦,吃不了逗著走个曙,直接拋出異常
if (Thread.currentThread() != this.getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException();
} else {
// 標(biāo)識鎖是不是已經(jīng)完全釋放了
boolean var3 = false;
// 沒有線程占用鎖了锈嫩,可以讓下一個線程來持鎖了
if (var2 == 0) {
// 鎖完全釋放了就返回true
var3 = true;
// 把鎖的持有者設(shè)置為null
this.setExclusiveOwnerThread((Thread)null);
}
// 更新state值
this.setState(var2);
return var3;
}
}
如果鎖完全釋放了,那么就得喚醒下家去搶鎖垦搬。具體是怎么尋找下家的呢呼寸,看一下unparkSuccessor()。
private void unparkSuccessor(Node var1) {
int var2 = var1.waitStatus;
if (var2 < 0) {
// 將頭節(jié)點(diǎn)設(shè)置為初始狀態(tài)
compareAndSetWaitStatus(var1, var2, 0);
}
Node var3 = var1.next;
if (var3 == null || var3.waitStatus > 0) {
var3 = null;
// 從隊列的末尾節(jié)點(diǎn)往前找下家猴贰,最終是找到隊列里(頭節(jié)點(diǎn)除外)最前面的節(jié)點(diǎn)对雪,作為喚醒對象
for(Node var4 = this.tail; var4 != null && var4 != var1; var4 = var4.prev) {
if (var4.waitStatus <= 0) {
var3 = var4;
}
}
}
// 喚醒這個節(jié)點(diǎn)
if (var3 != null) {
LockSupport.unpark(var3.thread);
}
}
非公平鎖到這里就講完了,至于tryLock()方法米绕,相信同學(xué)們在看完為上面lock()的分享瑟捣,已經(jīng)可以自己獨(dú)立把它拿下了,現(xiàn)在我們來講一下公平鎖栅干。前面已經(jīng)提到了公平鎖和非公平鎖的一個區(qū)別迈套,就是lock()里的tryAcquire()實(shí)現(xiàn)有所不同。非公平鎖任何一個新加入的線程都可以參與搶鎖碱鳞,但是公平鎖就得老老實(shí)實(shí)排隊桑李,講究個先來后到,具體來看下吧。
protected final boolean tryAcquire(int var1) {
Thread var2 = Thread.currentThread();
int var3 = this.getState();
if (var3 == 0) {
// hasQueuedPredecessors()很關(guān)鍵芙扎,它是公平性的核心體現(xiàn)
if (!this.hasQueuedPredecessors() && this.compareAndSetState(0, var1)) {
this.setExclusiveOwnerThread(var2);
return true;
}
} else if (var2 == this.getExclusiveOwnerThread()) {
// 鎖重入
int var4 = var3 + var1;
if (var4 < 0) {
throw new Error("Maximum lock count exceeded");
}
this.setState(var4);
return true;
}
// 搶鎖失敗了
return false;
}
hasQueuedPredecessors()的作用是當(dāng)滿足以下兩種條件中的一種時星岗,線程就能獲得搶鎖的資格: 1. 鎖同步隊列里只有一個節(jié)點(diǎn);2. 第二個節(jié)點(diǎn)屬于當(dāng)前線程戒洼。
設(shè)計思想
先看一下AQS內(nèi)部維護(hù)的鎖同步隊列俏橘。
ReentrantLock通過使用AQS來實(shí)現(xiàn)加解鎖。AQS內(nèi)部維護(hù)了一個雙向鏈表的鎖同步隊列圈浇,并維護(hù)頭節(jié)點(diǎn)head寥掐,尾節(jié)點(diǎn)tail和信號量state。每個節(jié)點(diǎn)是一個Node對象磷蜀,對象中定義了prev召耘,next分別指向它的上下游,還有一個waitStatus對象用于表示線程狀態(tài)(等鎖或已放棄)褐隆。當(dāng)有新的線程需要搶鎖時污它,新建一個和線程映射的Node,加入到鎖同步隊列的末尾庶弃。當(dāng)然這里有個重點(diǎn)衫贬,在加入的時候會做判斷,如果當(dāng)前末尾節(jié)點(diǎn)處于放棄狀態(tài)歇攻,那么會繼續(xù)往前遍歷固惯,尋找一個可靠的節(jié)點(diǎn)作為上游。AQS內(nèi)部的state為0時缴守,資源未被占用葬毫,線程可進(jìn)行CAS操作更新state,如果更新成功則代表加鎖成功屡穗。如果state不為0贴捡,則意味著資源已經(jīng)被線程占用。如果占用者是自己鸡捐,那么可以進(jìn)行重入栈暇,如果占用者不是自己,那么就老老實(shí)實(shí)等著箍镜。
關(guān)于ReentrantLock的源碼講解和原理分析,到這里就全部結(jié)束啦煎源。后續(xù)還會更新更多關(guān)于Java并發(fā)包的其他干貨色迂,同學(xué)們一定要結(jié)合起來閱讀,相輔相成手销,形成一個完整的知識體系歇僧。最后,喜歡我文章的同學(xué)們,歡迎關(guān)注我的公眾號《小瑾守護(hù)線程》诈悍,不錯過任何有價值的干貨祸轮。