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;
複製程式碼
這個變數是用來指向Sync
的子類的,也就是FairSync
或者NonfairSync
,這個也就是多型的父類引用指向子類,具體Sycn
指向哪個子類,看構造方法:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
複製程式碼
ReentrantLock
有兩個構造方法,無參構造方法預設是建立非公平鎖,而傳入true
為引數的構造方法建立的是公平鎖。
非公平鎖的實現原理
當我們使用無參構造方法構造的時候即ReentrantLock lock = new ReentrantLock()
,建立的就是非公平鎖。
public ReentrantLock() {
sync = new NonfairSync();
}
//或者傳入false引數 建立的也是非公平鎖
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
複製程式碼
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;
}
複製程式碼
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;
}
複製程式碼
公平鎖的實現原理
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;
}
複製程式碼
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);
}
複製程式碼
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);
}
}
複製程式碼
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();
}
複製程式碼
執行輸出:
執行緒獲取鎖----Thread-0
另外一個執行緒獲取到鎖----Thread-1
喚醒執行緒----Thread-1
另外一個執行緒釋放鎖----Thread-1
執行緒被喚醒----Thread-0
執行緒釋放鎖----Thread-0
複製程式碼
執行的流程大概是這樣,執行緒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();
複製程式碼
這樣就實現了部分喚醒的功能。
ReentrantLock和Synchronized對比
關於Synchronized
的介紹可以看《synchronized的使用(一)》、《深入分析synchronized原理和鎖膨脹過程(二)》
ReentrantLock | Synchronized | |
---|---|---|
底層實現 | 通過AQS 實現 |
通過JVM 實現,其中synchronized 又有多個型別的鎖,除了重量級鎖是通過monitor 物件(作業系統mutex互斥原語)實現外,其它型別的通過物件頭實現。 |
是否可重入 | 是 | 是 |
公平鎖 | 是 | 否 |
非公平鎖 | 是 | 是 |
鎖的型別 | 悲觀鎖、顯式鎖 | 悲觀鎖、隱式鎖(內建鎖) |
是否支援中斷 | 是 | 否 |
是否支援超時等待 | 是 | 否 |
是否自動獲取/釋放鎖 | 否 | 是 |
參考
《Java併發程式設計的藝術》
深入理解AbstractQueuedSynchronizer(AQS)
Java 重入鎖 ReentrantLock 原理分析)
原文地址:ddnd.cn/2019/03/24/…