JUC併發編程 - ReentrantLock源碼分析

前言

前面分析了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()),當前線程是否爲頭節點的後繼節點

返回情況有:

  1. 隊列還未初始化 false && (who care!)
  2. 隊列已經初始化過,並且 head沒有後繼節點 true && (false || who care!)
  3. 當前隊列存在有效節點s,並且s的線程與當前線程相同 ->這種情況我不認爲存在,不可能一個線程還在排隊,又操作一次申請鎖 true && (true || false)
  4. 當前隊列存在有效節點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的同步隊列,排隊獲取鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章