AQS應用——ReentrantLock源碼分析

前言

本文通過可重入鎖ReentrantLock的源碼分析,加深對aqs和ReentrantLock的理解
關於AQS相關的知識可以參考我的另一篇文章Java併發——AQS源碼解析

先從使用上入手

構造方法
public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

可以看到初始化了一個sync對象,而sync繼承了aqs

abstract static class Sync extends AbstractQueuedSynchronizer

默認初始化的是非公平鎖,構造方法傳遞true即爲公平鎖
先以默認的非公平鎖爲例,後面再總結公平鎖

加鎖
public void lock() {
    sync.lock();
}

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
            // aqs的模板方法
            // 加鎖失敗,說明其他線程先持有了鎖,acquire獲取鎖或者加入等待隊列
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

非公平鎖tryAcquire

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // 狀態爲0說明還沒有線程持有鎖
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 當前線程已經持有鎖
    else if (current == getExclusiveOwnerThread()) {
        // 重入鎖,狀態 +1
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

上面兩個條件都不滿足,直接返回false,進入等待隊列

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

protected final boolean tryRelease(int releases) {
    // 狀態減1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 是否釋放鎖的標誌
    boolean free = false;
    // 狀態爲0,則清空當前線程信息,標誌位置爲0
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 否則更新狀態,返回false
    setState(c);
    return free;
}

因爲是可重入鎖,同一個線程,每次加鎖狀態+1,釋放鎖,狀態-1,這也就是爲什麼對於同一個線程,lock和unlock調用的次數一定要一致

超時和響應中斷

aqs提供了超時和響應中斷的功能,ReentrantLock也提供了對應的方法
超時

public boolean tryLock(long timeout, TimeUnit unit)
        throws InterruptedException {
    // 直接調用aqs提供的方法
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

線程等待超時或被中斷,方法會立即返回
響應中斷

public void lockInterruptibly() throws InterruptedException {
    // 同樣也是直接調用aqs提供的獲取鎖響應中斷方法
    sync.acquireInterruptibly(1);
}

線程被中斷會方法立即返回
關於aqs這兩個方法的具體分析可以參考文章開頭提到的AQS源碼分析

newCondition

對於ReentrantLock我們通常也會使用其condition對象進行線程間的通信
作用類似於wait() 和 notify()

final ConditionObject newCondition() {
    return new ConditionObject();
}

newCondition初始化了AQS中的ConditionObject對象
ConditionObject有如下兩個屬性,我們可以知道,其內部也是維護了一個鏈表

/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;

直接從我們最常用的await和signal方法入手分析
ConditionObject#await
後續會涉及到兩個隊列,提前有個概念

  • sync同步隊列:指的是AQS中等待獲取鎖的node隊列
  • condition隊列:指的是調用condition#await方法,等待signal喚醒的node隊列
public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 添加到condition隊列
    Node node = addConditionWaiter();
    // await方法要完全釋放鎖
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 如果不在sync隊列(signal方法會將node放到sync隊列中),則阻塞線程
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 線程被喚醒,重新獲取鎖
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

然後我們按照方法細節再進行分析
ConditionObject#addConditionWaiter
添加線程到condition隊列

private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 如果尾節點取消的話,則移出隊列,獲取最後一個未取消的節點
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    // 第一次調用await會走到這裏,node狀態爲condition,表示在condition隊列
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

// 從頭到尾遍歷,移除已經取消的node節點
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;
            if (trail == null)
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            if (next == null)
                lastWaiter = trail;
        }
        else
            trail = t;
        t = next;
    }
}

假設此時有三個線程調用了condition的await方法阻塞,等待喚醒,那麼此時condition隊列的狀態如下

看到與aqs同步隊列的區別沒

  • 沒有空的head節點,firstWaiter代表第一個節點,lastWaiter代表最後一個節點
  • waitStatus爲Node.CONDITION
  • 雖然同樣是Node對象,但是並沒有賦值prev和next屬性,實際上是單向鏈表,用nextWaiter連接

ConditionObject#fullyRelease
完全釋放鎖,調用await方法,就代表要完全釋放當前持有的鎖(可重入)

final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        // 獲取當前鎖狀態
        int savedState = getState();
        // 完全釋放鎖
        if (release(savedState)) {
            failed = false;
            return savedState;
        } else {
            throw new IllegalMonitorStateException();
        }
    } finally {
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}

ConditionObject#isOnSyncQueue
判斷node是否在同步隊列中,如果在同步隊列則不阻塞線程

final boolean isOnSyncQueue(Node node) {
    // 如果狀態爲condition或者前驅節點爲null,則一定在condition隊列中
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    // 前後節點都有了,那一定在同步隊列了
    if (node.next != null) // If has successor, it must be on queue
        return true;
    /*
     * node.prev can be non-null, but not yet on queue because
     * the CAS to place it on queue can fail. So we have to
     * traverse from tail to make sure it actually made it.  It
     * will always be near the tail in calls to this method, and
     * unless the CAS failed (which is unlikely), it will be
     * there, so we hardly ever traverse much.
     */
    return findNodeFromTail(node);
}

// 在aqs同步隊列中,從tail開始向前找node,找到則返回true
private boolean findNodeFromTail(Node node) {
    Node t = tail;
    for (;;) {
        if (t == node)
            return true;
        if (t == null)
            return false;
        t = t.prev;
    }
}

如此看來,只有prev存在而next爲null的時候,纔會走到最後findNodeFromTail
註釋的意思是說prev不爲空,不能代表已經在同步隊列中,因爲會通過CAS將自己設置爲tail,可能會失敗,再回顧一下入隊操作

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 這裏將prev指向之前的tail,prev已經不爲空了
            node.prev = t;
            // cas將自己設置爲tail,這裏成功了纔是入隊列成功
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

阻塞前的方法都分析完成,接下來先分析下signal方法,便於更好的理解await方法
ConditionObject#signal
喚醒condition隊列第一個節點(未取消的)

public final void signal() {
    // 判斷是否持有鎖
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    // 第一個節點不爲空,執行通知
    if (first != null)
        doSignal(first);
}

private void doSignal(Node first) {
    do {
        // 如果只有一個節點,將lastWaiter置爲null
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        // 隊列中移除第一個節點
        first.nextWaiter = null;
    // 如果node轉換成功,則結束循環,否則繼續往後進行喚醒
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

// 將node從condition隊列中轉換到同步隊列中
final boolean transferForSignal(Node node) {
    // 修改狀態爲0,如果失敗說明被取消,返回false接着向後找
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 加入同步隊列,返回前驅節點p
    Node p = enq(node);
    int ws = p.waitStatus;
    // 如果前驅節點狀態大於0(已取消)或者CAS設置狀態爲SIGNAL失敗,則直接喚醒線程
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

當一個線程node在condition隊列中await時,如果被其他線程signal喚醒,那麼該node就需要加入aqs同步隊列中等待再次獲取鎖,用來執行await之後的代碼

所以signal方法概括來說就是,將condition隊列中的第一個節點移除,並將其加入aqs同步隊列中,用圖來表示會更直觀

這裏最令人迷惑的就是transferForSignal方法
最最迷惑的就是該方法中最後的條件判斷,喚醒操作

if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);

這個操作首先是node加入aqs同步隊列後,判斷它的前驅節點,也就是上圖轉移到aqs同步隊列之後的node-1的狀態

  • 如果 > 0 就直接喚醒Thread-1(剛轉移到同步隊列的線程)
  • 如果 <= 0,但是cas設置其狀態爲signal時失敗,也直接喚醒Thread-1

思考了好久纔有眉目,這裏爲什麼滿足這兩個條件要提前喚醒呢?

從正確性上來看,即使這裏不喚醒也不會出問題,因爲調用signal的線程一定會調用unlock方法,而unlock方法就會喚醒aqs同步隊列中的第一個非取消node,所以最終一定會傳遞下去,喚醒剛剛加入隊尾的node-2(Thread-1)

那我們回頭繼續看下await方法被喚醒之後的操作

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 添加到condition隊列
    Node node = addConditionWaiter();
    // await方法要完全釋放鎖
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // 如果不在sync隊列(signal方法會將node放到sync隊列中),則阻塞線程
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 線程被喚醒,重新獲取鎖
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    // 還有其他節點的話,遍歷清除取消的節點
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // 中斷後處理
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

被喚醒之後,加入aqs同步隊列,跳出while循環,調用acquireQueued方法
該方法可以看aqs源碼回憶一下,主要就這幾個步驟

  • 如果是第一個等待的節點,就嘗試獲取鎖
  • 如果不是,則會去判斷前驅節點的狀態,如果取消則刪除,向前找非取消的節點
  • 確保前驅節點未取消,且狀態爲signal,則將當前線程阻塞

如果transferForSignal中,判斷前驅節點已經取消,或者無法設置爲signal狀態,不提前喚醒
那麼等調用signal方法的線程unlock之後,去喚醒aqs同步隊列的節點
當一直喚醒到transferForSignal中轉移的節點之前時,還是要執行acquireQueued方法
處理前驅取消的節點,再設置狀態,而acquireQueued方法的調用是不需要持有獨佔鎖的
所以這裏爲了提高併發性能,讓acquireQueued方法和調用signal之後的操作同時進行
多看幾遍源碼就能理解,其實就是讓如下兩段代碼併發執行
被喚醒之後的aqs處理邏輯

signal喚醒之後的業務邏輯

喚醒之後對中斷異常的處理

private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}

final boolean transferAfterCancelledWait(Node node) {
    if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
        enq(node);
        return true;
    }
    /*
     * If we lost out to a signal(), then we can't proceed
     * until it finishes its enq().  Cancelling during an
     * incomplete transfer is both rare and transient, so just
     * spin.
     */
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

1、如果阻塞過程中線程沒被中斷,則返回0,且加入同步隊列中,退出循環
2、被中斷則修改狀態爲0,並加入到aqs同步隊列中
3、設置狀態失敗會自旋,直到加入到aqs同步隊列中
這裏返回的中斷模式有2種,主要是用來做不同的中斷後處理

/** Mode meaning to reinterrupt on exit from wait */
private static final int REINTERRUPT =  1;
/** Mode meaning to throw InterruptedException on exit from wait */
private static final int THROW_IE    = -1;

/**
 * Throws InterruptedException, reinterrupts current thread, or
 * does nothing, depending on mode.
 */
private void reportInterruptAfterWait(int interruptMode)
    throws InterruptedException {
    if (interruptMode == THROW_IE)
        throw new InterruptedException();
    else if (interruptMode == REINTERRUPT)
        selfInterrupt();
}

第1種情況是在喚醒後中斷,喚醒後狀態會被設置爲0,所以cas會失敗,自旋就是在等待加入aqs同步隊列之後再返回,否則await方法的循環會退出,可能還沒有調用enq方法入隊
Thread.interrupted()會清除中斷狀態,喚醒後重新interrupt

第2種情況就是在喚醒前中斷,狀態一定是CONDITION,最後返回-1
阻塞時中斷,拋出中斷異常

公平鎖

與非公平鎖的區別
lock

// 公平鎖
final void lock() {
	acquire(1);
}

// 非公平鎖
final void lock() {
	if (compareAndSetState(0, 1))
		setExclusiveOwnerThread(Thread.currentThread());
	else
		acquire(1);
}

首先從lock方法我們可以看到,公平鎖的區別就是每次獲取鎖都直接調用acquire去排隊,而非公平鎖,先嚐試cas獲取鎖,如果競爭到就持有鎖,不管隊列是否有其他等待的線程
tryAcquire

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;
}

如果當前沒有線程持有鎖,那麼要先判斷隊列中有沒有其他線程在等待獲取,不能直接嘗試獲取鎖

public final boolean hasQueuedPredecessors() {
	// The correctness of this depends on head being initialized
	// before tail and on head.next being accurate if the current
	// thread is first in queue.
	Node t = tail; // Read fields in reverse initialization order
	Node h = head;
	Node s;
	return h != t &&
		((s = h.next) == null || s.thread != Thread.currentThread());
}

h != t 說明有其他等待節點
(s = h.next) == null 這個條件什麼時候成立呢?
頭尾不相等,但是頭的next爲空,那就只有一種情況就是某個節點正在加入隊列過程中
還記得enq入隊方法麼?

Node t = tail;
...
node.prev = t;
if (compareAndSetTail(t, node)) {
	t.next = node;
	return t;
}

先將node的prev指向原來的tail,再通過cas更新tail引用,最後纔將前一個節點的next連起來
所以在t.next = node;執行前,cas成功後,會出現頭的next爲空
這種情況就直接返回true

如果不是的話,那麼需要判斷是不是當前線程在等待

總結

ReentrantLock源碼分析就結束了,回顧總結一下

  • ReentrantLock是jdk層面提供的獨佔鎖,基於AQS實現
  • 提供了公平與非公平兩種方式
    • 公平:lock時要判斷前面有沒有其他線程在等待,有先後順序
    • 非公平:lock時直接嘗試cas競爭鎖,不管前面是否有其他線程等待
  • 可重入鎖,同一個線程lock和unlock方法調用次數一定要對應,保證正確釋放鎖
  • 通過AQS的ConditionObject對象,提供了線程間通信的方法
  • await時加入condition隊列,釋放鎖
  • signal時喚醒節點,從condition隊列轉移到aqs同步隊列中,重新競爭鎖
  • 基於AQS,同樣的提供了響應中斷,獲取鎖超時方法
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章