前言
前面分析了AQS類的源碼,但真正實現AQS的實現類都在JUC中,當然AQS也是JUC的一部分,只是它不面向應用,除非自己去繼承實現一套邏輯。
在java的java.util.concurrent包,簡稱JUC,其內包含的類都與多線程有關,是非常重要的一個包。接下來準備針對JUC下常用類進行分析,剖析它們的原理及使用特點。而本文將針對比較常用的ReentrantLock源碼來分析。
ReentrantLock鎖是AQS的一種實現,它做到了可重入、可中斷,分爲公平和非公平兩類實現。在使用時,需要手動調用o.lock()和o.unlock()來加鎖和解鎖,並且解鎖是必須的。這種形式,將加鎖、解鎖的時機開放給了開發者,因此更加靈活。
類的定義
看一下ReentrantLock類的結構:
public class ReentrantLock implements Lock, java.io.Serializable
並沒繼承AQS,反而實現了Lock;因爲ReentrantLock是鎖,實現Lock很自然。看一下Lock接口的定義:
public interface Lock {
void lock();
void lockInterruptibly() throws InterruptedException;
boolean tryLock();
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
void unlock();
Condition newCondition();
}
加鎖、解鎖,還有一個方法用於創建Condition對象,這個在《xxx》有介紹過,可以移步加深一下印象。Condition與本文內容關係不大,回到ReentrantLock,看一下它內部有哪些成員。
成員變量
只有一個Sync類型的sync實例:
private final Sync sync;
內部類
Sync是內部類,它實現了AQS類,既然AQS成爲抽象隊列同步器,那麼我們可以稱Sync爲隊列同步器、同步器。
abstract static class Sync extends AbstractQueuedSynchronizer
另外還有兩個內部類,都是繼承自Sync:
static final class NonfairSync extends Sync
static final class FairSync extends Sync
從結構及名稱看,ReentrantLock實現了兩種鎖:公平同步器FairSync和非公平同步器NonfairSync,都源自AQS。
構造函數
兩個構造函數,默認無參的創建的是非公平同步器,還有一個根據入參來決定同步器類型:
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
實例方法
內部定義很多,重點看一下Lock接口的實現方法:
public void lock() {
sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
發現一個現象,都是調用的sync中的實現。那麼後面就對Sync的實現做重點分析。由於ReentrantLock默認實現非公平同步器,那麼就逐個分析NonfairSync類的實現。
非公平同步器NonfairSync
Lock接口的Lock()實現
實現的內容真少,看來主要內容還是在Sync裏。
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
從加鎖lock()方法看到,如果AQS.state=0,並且可以設置爲1,則設置線程持有者爲自己。
逐個邏輯第一次看可能有疑問,爲什麼state設置爲1就加鎖成功了? 因爲ReentrantLock是排它鎖的實現,而“非公平”的特點就是不管AQS排隊那一套,只要現在state=0,我就先去改一下值,改成功了鎖就是搶到了,否則再去走AQS.acquire(1)的流程。
看一看AQS的acquire():
public final void acquire(int arg) {
if (!tryAcquire(arg) && // t1
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在t1位置,AQS並沒有實現tryAcquire()方法,而是由Sync實現的,這在上面NonfairSync有,而且它調用的是Sync的nonfairTryAcquire()方法,調用鏈是:
lock()-->acquire()-->tryAcquire()-->nonfairTryAcquire()-->acquire()
看一下nonfairTryAcquire()方法的實現
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;
}
內容並不複雜,根據AQS中state的值,以及排它鎖持有者,來決定不同結果。
1、state==0,無鎖,直接CAS設置state的值,成功了就說明拿到鎖,否則肯定有其他線程申請到了。
2、state!=0,有鎖,如果持有者是自己,則對state的值累加,並且返回成功true
在2中的累加,含義是ReentrantLock是一個可重入鎖,持有鎖的線程可以多次申請鎖,但釋放鎖測次數要與申請次數相等,才能真正釋放鎖。
以上就是嘗試獲取非公平鎖tryAcquire的過程,再結合AQS中acquire()的實現,梳理下整個申請鎖過程。
介紹下AQS的大致結構:
1、包含頭尾指針的同步隊列head、tail
2、同步隊列的節點類Node,內部包含Thread線程對象、waitState節點狀態、同步隊列的前後指針prev、next
3、資源值state,將其成功改變的線程將會持有鎖
在AQS中,參與申請鎖流程的邏輯是, 申請鎖的線程會被封裝爲node對象,加入同步隊列中,之後會被掛起,當線程被喚醒後,會CAS方式修改state的值,也就是3中的流程;否則會再次掛起,這種自旋會一直持續,直到申請鎖成功、取消申請、線程中斷。
而申請流程的源碼如下,注意看註釋信息:
public final void acquire(int arg) {
if (!tryAcquire(arg) && // 如果前面返回false未獲得鎖,則進acquireQueued()
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // addWaiter加入同步隊列,再申請鎖
selfInterrupt(); // 中斷線程;當申請完成後,會返回線程狀態,true==線程中斷了,false==線程未中斷
}
// 加入同步隊列
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); // 封裝當前線程
// Try the fast path of enq; backup to full enq on failure
Node pred = tail; // 尾插法,直接加入同步隊列
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); // 再次for(;;)嘗試加入同步隊列
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { // 又調用tryAcquire()
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 以上,讓同步隊列頭節點的後繼節點,嘗試獲取鎖,成功的話將node調整爲頭節點
// 以下,獲取鎖失敗,將線程掛起,等待被喚醒
if (shouldParkAfterFailedAcquire(p, node) && // 檢查node狀態,決定是否應該被掛起
parkAndCheckInterrupt()) // 掛起線程,並在被喚醒後檢查線程是否中斷
interrupted = true; // parkAndCheckInterrupt返回true,代表線程中斷了
}
} finally {
if (failed)
cancelAcquire(node);
}
}
AQS詳細源碼分析,請移步《面試必考AQS-排它鎖的申請與釋放》等系列文章。
這裏再梳理一次非公平排它鎖的調用:
public final void acquire(int arg){...} // 獲取排它鎖的入口
# protected boolean tryAcquire(int arg); // 嘗試直接獲取鎖,這裏可以替換爲nonfairTryAcquire()
final boolean nonfairTryAcquire(int acquires) {...} // 非公平排它鎖實現tryAcquire(),執行完畢回到AQS流程
final boolean acquireQueued(final Node node, int arg) {...} // AQS中獲取排它鎖流程整合
private Node addWaiter(Node mode){...} // 將node加入到同步隊列的尾部
# protected boolean tryAcquire(int arg); // 如果當前node的前置節點pre變爲了head節點,則嘗試獲取鎖(因爲head可能正在釋放)
private void setHead(Node node) {...} // 設置 同步隊列的head節點
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {...} // 如果獲取鎖失敗,則整理隊中節點狀態,並判斷是否要將線程掛起
private final boolean parkAndCheckInterrupt() {...} // 將線程掛起,並在掛起被喚醒後檢查是否要中斷線程(返回是否中斷)
private void cancelAcquire(Node node) {...} // 取消當前節點獲取排它鎖,將其從同步隊列中移除
static void selfInterrupt() {...} // 操作將當前線程中斷
Lock接口的lockInterruptibly()是申請鎖過程允許中斷,當檢測到線程中斷時會拋出異常。
public void lockInterruptibly() throws InterruptedException {...}
具體邏輯與acquire()差別不太大,感興趣可以自行分析。
Lock接口的tryLock()實現
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
這個很有趣,它並沒有走AQS流程,而是走了tryAcquire()的具體實現nonfairTryAcquire(),也就是說,只是根據state的值、線程持有者,來確定是否申請到鎖,並沒有執行CLH模型的內容,在實際使用時要當心其語義。
Lock接口的tryLock(long time,TimeUnit unit)實現
這個實現主要源碼:
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();
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;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
它實際走的是AQS的帶超時時間的申請流程,內部在自旋過程中增加了事件的判斷,來決定是否繼續等待申請,並且它也是支持線程中斷的。
申請部分與acquireQueued()方法如出一轍,將與時間有關的代碼列出:
if (nanosTimeout <= 0L) // 非法值校驗
return false;
final long deadline = System.nanoTime() + nanosTimeout; // 獲取超時時刻
// ...
nanosTimeout = deadline - System.nanoTime(); // 每自旋一次,計算 剩餘申請時間
if (nanosTimeout <= 0L) // 如果剩餘時間<=0,結束
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold) // 掛起增加一個條件,剩餘時間要大於1000L
LockSupport.parkNanos(this, nanosTimeout); // 掛起時指定超時時間,超時自動結束掛起狀態
if (Thread.interrupted())
throw new InterruptedException();
static final long spinForTimeoutThreshold = 1000L;
比較特殊的地方是在掛起前判斷上,如果剩餘時間小於1000L,不會進行掛起操作,而是直接進入下一次循環,這個應該是考慮一次掛起喚醒的過程,耗時較高,剩餘時間可能不足,也是爲儘可能申請到鎖做努力。
Lock接口的unlock()實現
相關源碼
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
解鎖unlock()很簡單,直接調用的是AQS的release(),在Sync中並沒有重寫。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
看一下tryRelease()的實現
protected final boolean tryRelease(int releases) {
int c = getState() - releases; // 獲取需要釋放的state數量,與當前state值做差
if (Thread.currentThread() != getExclusiveOwnerThread()) // 如果當前線程沒有持有鎖
throw new IllegalMonitorStateException(); // 直接拋出異常
boolean free = false;
if (c == 0) { // 如果差值爲0,說明已經解鎖了
free = true;
setExclusiveOwnerThread(null); // 清空鎖的持有者
}
setState(c); // 修改state的值
return free;
}
在tryRelease()中,主要是修改state的值,如果沒到0,說明解鎖成功了,後面也就不用操作了,如果能修改到0,那麼後面還要繼續執行AQS的內容,去喚醒後繼節點申請鎖。
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0) // 判斷並修改節點狀態
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next; // 對於後繼節點,要遍歷出一個正常等待的節點來喚醒
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); // 喚醒後繼節點
}
Lock接口的newCondition()實現
這個方法就是返回一個Condition對象:
public Condition newCondition() {
return sync.newCondition();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
也就是說,Condition的使用,是通過ReentrantLock實現的,這也進一步驗證,在await()和signal()調用的場景,必須是持有鎖的場景,而鎖,就是創建Condition對象的ReentrantLock鎖持有的,這個在應用時一定要注意,不要ReentrantLock1 創建的Condition 在執行await()是,先申請ReentrantLock2的鎖,這就有問題了。
公平同步器FairSync
FairSync 的定義中,只有lock()和tryAcquire(),其中lock()並沒有像非公平同步器NonfairSync中,直接嘗試修改資源state,而是直接調用了AQS的acquire()。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
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;
}
}
在tryAcquire()中,有個!hasQueuedPredecessors()方法,在無鎖情況下,由它決定是否能直接去申請鎖:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
這個方法的目的是判斷 同步隊列的節點狀態,根據狀態:返回false,則會直接去申請鎖
AQS的同步隊列:head節點只是標識,並不記錄線程信息,當調用setHead(Node node)時,會清除node的前後節點指針。而enq()方法在初始化同步隊列時,是將tail指向了head,而之後添加節點時,是尾插法,也就是head不知道後置節點,而同步隊列中的節點都知道自己的前置節點。
結合同步隊列添加節點的方法:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 初始化隊列
if (compareAndSetHead(new Node()))
tail = head; // 頭尾相等
} else { // 尾插加入
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
回看釋放鎖的代碼,是沒有設置head節點的,也就是說當釋放完鎖,如果沒有後繼節點可被喚醒,head節點將保持最後一次加鎖時設置的值;也就是除了都爲null 以及 首次初始化還未來得及添加節點時head==tail,其他時刻都head!=tail。
分析一下這個方法的判斷
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
(h!=t)==true,頭尾節點不相等,說明同步隊列已經初始化過
(h!=t)==false,頭尾節點相等,上面分析的情況【導致外層方法if判斷通過,嘗試獲取鎖】
((s = h.next) == null)==true,頭節點沒有後繼節點,可能都已經出隊
((s = h.next) == null)==false,頭節點有後繼節點
(s.thread != Thread.currentThread()),當前線程是否爲頭節點的後繼節點
返回情況有:
- 隊列還未初始化 false && (who care!)
- 隊列已經初始化過,並且 head沒有後繼節點 true && (false || who care!)
- 當前隊列存在有效節點s,並且s的線程與當前線程相同 ->這種情況我不認爲存在,不可能一個線程還在排隊,又操作一次申請鎖 true && (true || false)
- 當前隊列存在有效節點s,並且s的線程與當前線程不相同 --> 這種情況 會讓當前線程進入同步隊列,這種情況是在同步隊列中有節點正在申請鎖,而還未申請完成state==0,又有新線程來競爭,這種情況必須入隊。 true && (true || true)
上述情況1\2\3下,會導致方法返回false,進而導致外層去直接CAS申請資源;而情況4則會去排隊。
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
至於公平同步器的解鎖,它直接使用的是Sync的tryRelease(),流程上面已經介紹過。
尾聲
整個ReentrantLock由兩部分組成,一個是實現AQS的Sync同步器,再一個是自身實現的Lock接口,並由Sync同步器去做具體實現。由Sync定義tryAcquire()和tryRelease(),也就是state如何操作、變爲何種值纔算加鎖成功,否則進入AQS的同步隊列,排隊獲取鎖。