從源碼分析ReentrantLock基本原理

從源碼分析ReentrantLock基本原理

記錄併發編程學習中,關於ReentrantLock可重入鎖的加鎖和釋放鎖過程。分析加鎖和釋放鎖代碼邏輯,瞭解其基本實現原理,對於分析過程中的理解誤點,麻煩不吝賜教。

本次分析代碼版本jdk1.8。

ReentrantLock基本介紹

在實際的開發場景中,synchronized的加鎖場景存在不夠靈活的侷限性,因此java提供了Lock接口。ReentrantLock是對Lock接口的實現,synchronized和ReentrantLock都是可重入鎖的實現。

AbstractQueuedSynchronizer分析

理解ReentrantLock的實現原理需要先去了解AbstractQueuedSynchronizer(後續統一稱爲AQS)的幾個基本概念和其維護的雙向鏈表數據結構。AQS是java實現同步的工具也是Lock用來實現線程同步的核心,在AQS中提供兩種模型,一個是獨佔鎖,一個是共享鎖,本次分析的ReentrantLock就是獨佔鎖的實現。

獨佔鎖、共享鎖

獨佔鎖可以簡單理解爲,每次對於加了鎖的代碼,同時只能有一個線程訪問(同時只有一個線程持有鎖資源),而共享鎖則是允許多個線程。

鎖資源同步狀態
/**
 * The synchronization state.
 */
private volatile int state;

state這個變量,可以理解是對鎖資源的佔用標識,當鎖資源沒有線程佔用的情況下,state爲0的情況標識當前沒有線程持有鎖資源。

雙向鏈表

AQS底層維護一個FIFO的雙向鏈表,鏈表由AQS實現內部類封裝爲一個Node對象,這樣設計方便快速定義鏈表的首尾節點。鏈表定義見如下源碼。

/**
 * Head of the wait queue, lazily initialized.  Except for
 * initialization, it is modified only via method setHead.  Note:
 * If head exists, its waitStatus is guaranteed not to be
 * CANCELLED.
 */
private transient volatile Node head;

/**
 * Tail of the wait queue, lazily initialized.  Modified only via
 * method enq to add new wait node.
 */
private transient volatile Node tail;
節點

上面可以看到,雙向鏈表是由node對象的封裝。在node對象,主要是鏈表的數據結構指向鏈的prev和next,並封裝了線程對象在node中。另外一個需要關注的點是node中的成員變量waitStatus,這個狀態標識node的狀態,用於後續AQS對鏈表的維護。瞭解了AQS中的幾個基本概念,現在開始分析ReentrantLock的加鎖過程。

ReentrantLock加鎖過程分析

調用ReentrantLock加鎖方法lock(),可以觀察到是調用sync.lock(),而sync是繼承自AQS的抽象類,sync的實現類有兩個,FairSync(公平鎖)和NonfairSync(非公平鎖)。對於公平鎖和非公平鎖可以理解爲,公平鎖是FIFO,先來的線程會優先獲得鎖,具體過程見如下源碼。

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

//NonfairSync
final void lock() {
    		//這段可以理解爲,非公平鎖的實現會在程序調用lock加鎖代碼的時候,以插隊的形式先嚐試去搶一下鎖資源
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
}

核心的實現acquire(1)方法是在AQS中的實現。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

可以看到程序先調用了tryAcquire,而在AQS中,tryAcquire只是拋出了一個異常,從官方的說明理解是設計者認爲不支持排他鎖的實現無需調用這個方法。因此回到tryAcquire在ReentrantLock使用時的具體實現,是由FairSync和NonfairSync來實現的。

/**
 * 公平鎖實現
 * 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;
}

/**
 * 非公平鎖實現
 * Performs non-fair tryLock.  tryAcquire is implemented in
 * subclasses, but both need nonfair try for trylock method.
 */
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;
}

這裏也可以看到,對於公平鎖和非公平鎖,會有一個排隊獲取的問題,在公平鎖中,通過hasQueuedPredecessors方法判斷當前線程在AQS中的時間順序來確認是否允許去佔鎖資源。而非公平鎖還是直接插隊搶佔。這裏需要注意state這個變量,state爲0時表示當前沒有線程佔用鎖,而大於0時表示鎖佔用次數(此處大於0的情況爲重入鎖的實現,線程重入時,state會+1)。如果cas成功,纔會將當前線程標識爲鎖的持有線程。

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

回到acquire方法,在嘗試獲得鎖失敗後,會去調用addWaiter方法,將線程加入鏈表。接下來分析addWaiter源碼。

/**
 * Creates and enqueues node for current thread and given mode.
 *
 * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
 * @return the new node
 */
private Node addWaiter(Node mode) {
    //將線程封裝爲node
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    //鏈表尾部節點不爲空的情況下,將node設置爲尾部節點,將之前尾部節點的next指向當前node
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //enq理解類似初始化的實現,對AQS鏈表做初始化並設置當前node爲尾部節點
    enq(node);
    return node;
}

addWaiter中的實現,首先會將當前線程封裝爲node節點,然後將node節點設置爲AQS中的尾部節點。完成後調用acquireQueued方法,接下來分析下acquireQueued的源碼。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //自旋
            for (;;) {
                //獲取上一個節點
                final Node p = node.predecessor();
                //如果上個節點爲head節點,嘗試獲取鎖成功。
                if (p == head && tryAcquire(arg)) {
                    //設置當前節點爲head節點
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //這裏是判斷節點狀態並從鏈表中移除無效節點,完成後,掛起當前線程
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}

在acquireQueued中,通過自旋的方式去校驗當前節點是否處於就緒狀態,也就是當前節點的上一個節點爲head,並且通過tryAcquire方法成功佔用鎖,這時表名head節點執行完成並已經釋放鎖資源,然後將當前節點設置爲head節點。如果當前節點的上一個節點不爲head說明在當前節點之前還有入隊時間更早的節點,需要排隊。因此進入shouldParkAfterFailedAcquire方法來檢查節點狀態並通過unsafe包內提供的native方法來掛起線程。可以分析一下兩個方法的代碼。

/**
 * Checks and updates status for a node that failed to acquire.
 * Returns true if thread should block. This is the main signal
 * control in all acquire loops.  Requires that pred == node.prev.
 *
 * @param pred node's predecessor holding status
 * @param node the node
 * @return {@code true} if thread should block
 */
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //拿到上一個節點的狀態
    int ws = pred.waitStatus;
    //如果是SIGNAL狀態,標識上一個節點狀態有效,會返回true,然後讓當前節點去掛起排隊
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    //如果狀態>0,表示上一個節點等待超時或者被中斷過了,因此會從鏈表中移除上一個節點並循環往上檢查節點有效性
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        //將上一個節點的狀態置爲SIGNAL,標識有效
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

shouldParkAfterFailedAcquire方法用於判斷當前pred節點的狀態,如果pred節點狀態爲CANCELLED(1),則會從AQS隊列中移除並將當前node指向pred節點的prev節點。如果上一個節點狀態有效,則進入parkAndCheckInterrupt()方法。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

parkAndCheckInterrupt()用於掛起當前線程。而掛起後返回線程的中斷狀態,這裏可以理解爲,因爲park掛起後線程處於阻塞狀態,而在阻塞狀態下要麼是被被unpark喚醒或者是被中斷喚醒,因此去獲取中斷標識用於判斷。這塊的理解可以單獨閱讀關於park和interrupted的基本原理。如果返回標識爲中斷,後續會走selfInterrupt()方法。

附上加鎖邏輯時序圖。

ReentrantLock工作原理時序圖(獲得鎖)

ReentrantLock釋放鎖過程分析

釋放鎖的流程相對於加鎖簡單一些,公平鎖和非公平鎖的釋放鎖流程統一有AQS實現,接下來開始分析鎖的釋放代碼和過程。可以先看到AQS中的release方法。

/**
 * Releases in exclusive mode.  Implemented by unblocking one or
 * more threads if {@link #tryRelease} returns true.
 * This method can be used to implement method {@link Lock#unlock}.
 *
 * @param arg the release argument.  This value is conveyed to
 *        {@link #tryRelease} but is otherwise uninterpreted and
 *        can represent anything you like.
 * @return the value returned from {@link #tryRelease}
 */
public final boolean release(int arg) {
    //釋放鎖資源
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //喚醒排隊中的head的next節點。
            unparkSuccessor(h);
        return true;
    }
    return false;
}

在release方法中可以看到,主要的邏輯在於tryRelease和unparkSuccessor中。可以先看一下tryRelease方法。

protected final boolean tryRelease(int releases) {
    //鎖資源佔位標識-1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        //如果資源釋放完成返回true,並且釋放線程佔用標識
        free = true;
        setExclusiveOwnerThread(null);
    }
    //重新設置鎖資源佔位標識
    setState(c);
    return free;
}

可以看到在ReentrantLock中的tryRelease實現,首先會去對state - 1,這裏是重入鎖的設計,多次重入的情況下需要對應多次的鎖資源釋放,在全部釋放完成後state = 0,然後設置當前獨佔鎖的佔用線程爲null(也是釋放資源),並重新設置鎖的資源站位標識爲0並返回true。而當返回true之後,開始去喚醒鏈表中排隊的節點了。可以看到unparkSuccessor方法。

/**
 * Wakes up node's successor, if one exists.
 *
 * @param node the node
 */
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);
}

這個方法最主要的功能也就是最後這句LockSupport.unpark了。喚醒的節點是當前節點的next節點,在上面會對next做一次有效性掃描,waitStatus >0的情況上面提到過是屬於CANCELLED狀態。那麼,當next不爲CANCELLED後,就開始喚醒節點線程了。這個時候鎖的釋放就完成了,那麼可以回過去看一下acquireQueued方法,之前被阻塞的線程會被重新喚醒,然後再次去對鎖資源競爭。

附上釋放鎖的邏輯時序圖。

ReentrantLock工作原理時序圖(釋放鎖)

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