深入理解ReentrantLock的實現原理


文章目錄
ReentrantLock簡介
AQS回顧
ReentrantLock原理
ReentrantLock結構
非公平鎖的實現原理
lock方法獲取鎖
tryRelease鎖的釋放
公平鎖的實現原理
lock方法獲取鎖
tryRelease鎖的釋放
lockInterruptibly可中斷方式獲取鎖
tryLock超時等待方式獲取鎖
ReentrantLock的等待/通知機制
ReentrantLock和Synchronized對比
參考
ReentrantLock簡介
ReentrantLock是Java在JDK1.5引入的顯式鎖,在實現原理和功能上都和內置鎖(synchronized)上都有區別,在文章最后我們再比較這兩個鎖。
首先我們要知道ReentrantLock是基於AQS實現的,所以我們得對AQS有所了解才能更好的去學習掌握ReentrantLock,關於AQS的介紹可以參考我之前寫的一篇文章《一文帶你快速掌握AQS》,這里簡單回顧下AQS。

AQS回顧
AQS即AbstractQueuedSynchronizer的縮寫,這個是個內部實現了兩個隊列的抽象類,分別是同步隊列和條件隊列。其中同步隊列是一個雙向鏈表,里面儲存的是處於等待狀態的線程,正在排隊等待喚醒去獲取鎖,而條件隊列是一個單向鏈表,里面儲存的也是處於等待狀態的線程,只不過這些線程喚醒的結果是加入到了同步隊列的隊尾,AQS所做的就是管理這兩個隊列里面線程之間的等待狀態-喚醒的工作。
在同步隊列中,還存在2中模式,分別是獨占模式和共享模式,這兩種模式的區別就在於AQS在喚醒線程節點的時候是不是傳遞喚醒,這兩種模式分別對應獨占鎖和共享鎖。
AQS是一個抽象類,所以不能直接實例化,當我們需要實現一個自定義鎖的時候可以去繼承AQS然后重寫獲取鎖的方式和釋放鎖的方式還有管理state,而ReentrantLock就是通過重寫了AQS的tryAcquire和tryRelease方法實現的lock和unlock。

 


ReentrantLock原理
通過前面的回顧,是不是對ReentrantLock有了一定的了解了,ReentrantLock通過重寫鎖獲取方式和鎖釋放方式這兩個方法實現了公平鎖和非公平鎖,那么ReentrantLock是怎么重寫的呢,這也就是本節需要探討的問題。

ReentrantLock結構

 

 

首先ReentrantLock繼承自父類Lock,然后有3個內部類,其中Sync內部類繼承自AQS,另外的兩個內部類繼承自Sync,這兩個類分別是用來公平鎖和非公平鎖的。
通過Sync重寫的方法tryAcquire、tryRelease可以知道,ReentrantLock實現的是AQS的獨占模式,也就是獨占鎖,這個鎖是悲觀鎖。

ReentrantLock有個重要的成員變量:

private final Sync sync;
1
這個變量是用來指向Sync的子類的,也就是FairSync或者NonfairSync,這個也就是多態的父類引用指向子類,具體Sycn指向哪個子類,看構造方法:

public ReentrantLock() {
sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
1
2
3
4
5
6
7
ReentrantLock有兩個構造方法,無參構造方法默認是創建非公平鎖,而傳入true為參數的構造方法創建的是公平鎖。

非公平鎖的實現原理
當我們使用無參構造方法構造的時候即ReentrantLock lock = new ReentrantLock(),創建的就是非公平鎖。

public ReentrantLock() {
sync = new NonfairSync();
}

//或者傳入false參數 創建的也是非公平鎖
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
1
2
3
4
5
6
7
8
lock方法獲取鎖
lock方法調用CAS方法設置state的值,如果state等於期望值0(代表鎖沒有被占用),那么就將state更新為1(代表該線程獲取鎖成功),然后執行setExclusiveOwnerThread方法直接將該線程設置成鎖的所有者。如果CAS設置state的值失敗,即state不等於0,代表鎖正在被占領着,則執行acquire(1),即下面的步驟。
nonfairTryAcquire方法首先調用getState方法獲取state的值,如果state的值為0(之前占領鎖的線程剛好釋放了鎖),那么用CAS這是state的值,設置成功則將該線程設置成鎖的所有者,並且返回true。如果state的值不為0,那就調用getExclusiveOwnerThread方法查看占用鎖的線程是不是自己,如果是的話那就直接將state + 1,然后返回true。如果state不為0且鎖的所有者又不是自己,那就返回false,然后線程會進入到同步隊列中。

 

 

 


final void lock() {
//CAS操作設置state的值
if (compareAndSetState(0, 1))
//設置成功 直接將鎖的所有者設置為當前線程 流程結束
setExclusiveOwnerThread(Thread.currentThread());
else
//設置失敗 則進行后續的加入同步隊列准備
acquire(1);
}

public final void acquire(int arg) {
//調用子類重寫的tryAcquire方法 如果tryAcquire方法返回false 那么線程就會進入同步隊列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

//子類重寫的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
//調用nonfairTryAcquire方法
return nonfairTryAcquire(acquires);
}

final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//如果狀態state=0,即在這段時間內 鎖的所有者把鎖釋放了 那么這里state就為0
if (c == 0) {
//使用CAS操作設置state的值
if (compareAndSetState(0, acquires)) {
//操作成功 則將鎖的所有者設置成當前線程 且返回true,也就是當前線程不會進入同步
//隊列。
setExclusiveOwnerThread(current);
return true;
}
}
//如果狀態state不等於0,也就是有線程正在占用鎖,那么先檢查一下這個線程是不是自己
else if (current == getExclusiveOwnerThread()) {
//如果線程就是自己了,那么直接將state+1,返回true,不需要再獲取鎖 因為鎖就在自己
//身上了。
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//如果state不等於0,且鎖的所有者又不是自己,那么線程就會進入到同步隊列。
return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
tryRelease鎖的釋放
判斷當前線程是不是鎖的所有者,如果是則進行步驟2,如果不是則拋出異常。
判斷此次釋放鎖后state的值是否為0,如果是則代表鎖有沒有重入,然后將鎖的所有者設置成null且返回true,然后執行步驟3,如果不是則代表鎖發生了重入執行步驟4。
現在鎖已經釋放完,即state=0,喚醒同步隊列中的后繼節點進行鎖的獲取。
鎖還沒有釋放完,即state!=0,不喚醒同步隊列。

 

 

 


public void unlock() {
sync.release(1);
}

public final boolean release(int arg) {
//子類重寫的tryRelease方法,需要等鎖的state=0,即tryRelease返回true的時候,才會去喚醒其
//它線程進行嘗試獲取鎖。
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

protected final boolean tryRelease(int releases) {
//狀態的state減去releases
int c = getState() - releases;
//判斷鎖的所有者是不是該線程
if (Thread.currentThread() != getExclusiveOwnerThread())
//如果所的所有者不是該線程 則拋出異常 也就是鎖釋放的前提是線程擁有這個鎖,
throw new IllegalMonitorStateException();
boolean free = false;
//如果該線程釋放鎖之后 狀態state=0,即鎖沒有重入,那么直接將將鎖的所有者設置成null
//並且返回true,即代表可以喚醒其他線程去獲取鎖了。如果該線程釋放鎖之后state不等於0,
//那么代表鎖重入了,返回false,代表鎖還未正在釋放,不用去喚醒其他線程。
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
公平鎖的實現原理
lock方法獲取鎖
獲取狀態的state的值,如果state=0即代表鎖沒有被其它線程占用(但是並不代表同步隊列沒有線程在等待),執行步驟2。如果state!=0則代表鎖正在被其它線程占用,執行步驟3。
判斷同步隊列是否存在線程(節點),如果不存在則直接將鎖的所有者設置成當前線程,且更新狀態state,然后返回true。
判斷鎖的所有者是不是當前線程,如果是則更新狀態state的值,然后返回true,如果不是,那么返回false,即線程會被加入到同步隊列中
通過步驟2實現了鎖獲取的公平性,即鎖的獲取按照先來先得的順序,后來的不能搶先獲取鎖,非公平鎖和公平鎖也正是通過這個區別來實現了鎖的公平性。

 

 

 

 

final void lock() {
acquire(1);
}

public final void acquire(int arg) {
//同步隊列中有線程 且 鎖的所有者不是當前線程那么將線程加入到同步隊列的尾部,
//保證了公平性,也就是先來的線程先獲得鎖,后來的不能搶先獲取。
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//判斷狀態state是否等於0,等於0代表鎖沒有被占用,不等於0則代表鎖被占用着。
if (c == 0) {
//調用hasQueuedPredecessors方法判斷同步隊列中是否有線程在等待,如果同步隊列中沒有
//線程在等待 則當前線程成為鎖的所有者,如果同步隊列中有線程在等待,則繼續往下執行
//這個機制就是公平鎖的機制,也就是先讓先來的線程獲取鎖,后來的不能搶先獲取。
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//判斷當前線程是否為鎖的所有者,如果是,那么直接更新狀態state,然后返回true。
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
//如果同步隊列中有線程存在 且 鎖的所有者不是當前線程,則返回false。
return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
tryRelease鎖的釋放
公平鎖的釋放和非公平鎖的釋放一樣,這里就不重復。
公平鎖和非公平鎖的公平性是在獲取鎖的時候體現出來的,釋放的時候都是一樣釋放的。

lockInterruptibly可中斷方式獲取鎖
ReentrantLock相對於Synchronized擁有一些更方便的特性,比如可以中斷的方式去獲取鎖。

public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}

public final void acquireInterruptibly(int arg)
throws InterruptedException {
//如果當前線程已經中斷了,那么拋出異常
if (Thread.interrupted())
throw new InterruptedException();
//如果當前線程仍然未成功獲取鎖,則調用doAcquireInterruptibly方法,這個方法和
//acquireQueued方法沒什么區別,就是線程在等待狀態的過程中,如果線程被中斷,線程會
//拋出異常。
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tryLock超時等待方式獲取鎖
ReentrantLock除了能以能中斷的方式去獲取鎖,還可以以超時等待的方式去獲取鎖,所謂超時等待就是線程如果在超時時間內沒有獲取到鎖,那么就會返回false,而不是一直"死循環"獲取。

判斷當前節點是否已經中斷,已經被中斷過則拋出異常,如果沒有被中斷過則嘗試獲取鎖,獲取失敗則調用doAcquireNanos方法使用超時等待的方式獲取鎖。
將當前節點封裝成獨占模式的節點加入到同步隊列的隊尾中。
進入到"死循環"中,但是這個死循環是有個限制的,也就是當線程達到超時時間了仍未獲得鎖,那么就會返回false,結束循環。這里調用的是LockSupport.parkNanos方法,在超時時間內沒有被中斷,那么線程會從超時等待狀態轉成了就緒狀態,然后被CPU調度繼續執行循環,而這時候線程已經達到超時等到的時間,返回false。
LockSuport的方法能響應Thread.interrupt,但是不會拋出異常

 

 

 

 

public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//如果當前線程已經中斷了 則拋出異常
if (Thread.interrupted())
throw new InterruptedException();
//再嘗試獲取一次 如果不成功則調用doAcquireNanos方法進行超時等待獲取鎖
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}

private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
//計算超時的時間 即當前虛擬機的時間+設置的超時時間
final long deadline = System.nanoTime() + nanosTimeout;
//調用addWaiter將當前線程封裝成獨占模式的節點 並且加入到同步隊列尾部
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
//如果當前節點的前驅節點為頭結點 則讓當前節點去嘗試獲取鎖。
if (p == head && tryAcquire(arg)) {
//當前節點獲取鎖成功 則將當前節點設置為頭結點,然后返回true。
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
//如果當前節點的前驅節點不是頭結點 或者 當前節點獲取鎖失敗,
//則再次判斷當前線程是否已經超時。
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
//調用shouldParkAfterFailedAcquire方法,告訴當前節點的前驅節點 我要進入
//等待狀態了,到我了記得喊我,即做好進入等待狀態前的准備。
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
//調用LockSupport.parkNanos方法,將當前線程設置成超時等待的狀態。
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
ReentrantLock的等待/通知機制
我們知道關鍵字Synchronized + Object的wait和notify、notifyAll方法能實現等待/通知機制,那么ReentrantLock是否也能實現這樣的等待/通知機制,答案是:可以。
ReentrantLock通過Condition對象,也就是條件隊列實現了和wait、notify、notifyAll相同的語義。
線程執行condition.await()方法,將節點1從同步隊列轉移到條件隊列中。

 

 

 

線程執行condition.signal()方法,將節點1從條件隊列中轉移到同步隊列。

 

 


因為只有在同步隊列中的線程才能去獲取鎖,所以通過Condition對象的wait和signal方法能實現等待/通知機制。
代碼示例:

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
public void await() {
lock.lock();
try {
System.out.println("線程獲取鎖----" + Thread.currentThread().getName());
condition.await(); //調用await()方法 會釋放鎖,和Object.wait()效果一樣。
System.out.println("線程被喚醒----" + Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("線程釋放鎖----" + Thread.currentThread().getName());
}
}

public void signal() {
try {
Thread.sleep(1000); //休眠1秒鍾 等等一個線程先執行
} catch (InterruptedException e) {
e.printStackTrace();
}
lock.lock();
try {
System.out.println("另外一個線程獲取到鎖----" + Thread.currentThread().getName());
condition.signal();
System.out.println("喚醒線程----" + Thread.currentThread().getName());
} finally {
lock.unlock();
System.out.println("另外一個線程釋放鎖----" + Thread.currentThread().getName());
}
}

public static void main(String[] args) {
Test t = new Test();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
t.await();
}
});

Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
t.signal();
}
});

t1.start();
t2.start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
運行輸出:

線程獲取鎖----Thread-0
另外一個線程獲取到鎖----Thread-1
喚醒線程----Thread-1
另外一個線程釋放鎖----Thread-1
線程被喚醒----Thread-0
線程釋放鎖----Thread-0
1
2
3
4
5
6
執行的流程大概是這樣,線程t1先獲取到鎖,輸出了"線程獲取鎖----Thread-0",然后線程t1調用await方法,調用這個方法的結果就是線程t1釋放了鎖進入等待狀態,等待喚醒,接下來線程t2獲取到鎖,然輸出了"另外一個線程獲取到鎖----Thread-1",同時線程t2調用signal方法,調用這個方法的結果就是喚醒一個在條件隊列(Condition)的線程,然后線程t1被喚醒,而這個時候線程t2並沒有釋放鎖,線程t1也就沒法獲得鎖,等線程t2繼續執行輸出"喚醒線程----Thread-1"之后線程t2釋放鎖且輸出"另外一個線程釋放鎖----Thread-1",這時候線程t1獲得鎖,繼續往下執行輸出了線程被喚醒----Thread-0,然后釋放鎖輸出"線程釋放鎖----Thread-0"。

如果想單獨喚醒部分線程應該怎么做呢?這時就有必要使用多個Condition對象了,因為ReentrantLock支持創建多個Condition對象,例如:

//為了減少篇幅 僅給出偽代碼
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
Condition condition1 = lock.newCondition();

//線程1 調用condition.await() 線程進入到條件隊列
condition.await();

//線程2 調用condition1.await() 線程進入到條件隊列
condition1.await();

//線程32 調用condition.signal() 僅喚醒調用condition中的線程,不會影響到調用condition1。
condition1.await();
1
2
3
4
5
6
7
8
9
10
11
12
13
這樣就實現了部分喚醒的功能。

ReentrantLock和Synchronized對比
關於Synchronized的介紹可以看《synchronized的使用(一)》、《深入分析synchronized原理和鎖膨脹過程(二)》

ReentrantLock Synchronized
底層實現 通過AQS實現 通過JVM實現,其中synchronized又有多個類型的鎖,除了重量級鎖是通過monitor對象(操作系統mutex互斥原語)實現外,其它類型的通過對象頭實現。
是否可重入 是 是
公平鎖 是 否
非公平鎖 是 是
鎖的類型 悲觀鎖、顯式鎖 悲觀鎖、隱式鎖(內置鎖)
是否支持中斷 是 否
是否支持超時等待 是 否
是否自動獲取/釋放鎖 否 是
參考

 

 

 

 


————————————————
版權聲明:本文為CSDN博主「薛8」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/xueba8/article/details/88792394


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM