Java併發(5)- ReentrantLock與AQS

knock_小新發表於2019-03-04

引言

synchronized未優化之前,我們在編碼中使用最多的同步工具類應該是ReentrantLock類,ReentrantLock擁有優化後synchronized關鍵字的效能,又提供了更多的靈活性。相比synchronized,他在功能上更加強大,具有等待可中斷,公平鎖以及繫結多個條件等synchronized不具備的功能,是我們開發過程中必須要重點掌握的一個關鍵併發類。

ReentrantLock在JDK併發包中舉足輕重,不僅是因為他本身的使用頻度,同時他也為大量JDK併發包中的併發類提供底層支援,包括CopyOnWriteArrayLitCyclicBarrierLinkedBlockingDeque等等。既然ReentrantLock如此重要,那麼瞭解他的底層實現原理對我們在不同場景下靈活使用ReentrantLock以及查詢各種併發問題就很關鍵。這篇文章就帶領大家一步步剖析ReentrantLock底層的實現邏輯,瞭解實現邏輯之後又應該怎麼更好的使用ReentrantLock

ReentrantLock與AbstractQueuedSynchronizer的關係

在使用ReentrantLock類時,第一步就是對他進行例項化,也就是使用new ReentrantLock(),來看看他的例項化的原始碼:

public ReentrantLock() {
    sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
複製程式碼

在程式碼中可以看到,ReentrantLock提供了2個例項化方法,未帶引數的例項化方法預設用NonfairSync()初始化了sync欄位,帶引數的例項化方法通過引數區用NonfairSync()FairSync()初始化sync欄位。

通過名字看出也就是我們常用的非公平鎖與公平鎖的實現,公平鎖需要通過排隊FIFO的方式來獲取鎖,非公平鎖也就是說可以插隊,預設情況下ReentrantLock會使用非公平鎖的實現。那麼是sync欄位的實現邏輯是什麼呢?看下sync的程式碼:

private final Sync sync;

abstract static class Sync extends AbstractQueuedSynchronizer {......}

static final class NonfairSync extends Sync {......}

static final class FairSync extends Sync {......}
複製程式碼

到這裡就發現了AbstractQueuedSynchronizer類,公平鎖和非公平鎖其實都是在AbstractQueuedSynchronizer的基礎上實現的,也就是AQS。AQS提供了ReentrantLock實現的基礎。

ReentrantLock的lock()方法

分析了ReentrantLock的例項化之後,來看看他是怎麼實現鎖這個功能的:

//ReentrantLock的lock方法
public void lock() {
    sync.lock();
}

//呼叫了Sync中的lock抽象方法
abstract static class Sync extends AbstractQueuedSynchronizer {
    ......
    /**
        * Performs {@link Lock#lock}. The main reason for subclassing
        * is to allow fast path for nonfair version.
        */
    abstract void lock();
    ......
}
複製程式碼

呼叫了synclock()方法,Sync類的lock()方法是一個抽象方法,NonfairSync()FairSync()分別對lock()方法進行了實現。

//非公平鎖的lock實現
static final class NonfairSync extends Sync {
    ......
    /**
        * Performs lock.  Try immediate barge, backing up to normal
        * acquire on failure.
        */
    final void lock() {
        if (compareAndSetState(0, 1)) //插隊操作,首先嚐試CAS獲取鎖,0為鎖空閒
            setExclusiveOwnerThread(Thread.currentThread()); //獲取鎖成功後設定當前執行緒為佔有鎖執行緒
        else
            acquire(1);
    }
    ......
}

//公平鎖的lock實現
static final class FairSync extends Sync {
    ......
    final void lock() {
        acquire(1);
    }
    ......
}
複製程式碼

注意看他們的區別,NonfairSync()會先進行一個CAS操作,將一個state狀態從0設定到1,這個也就是上面所說的非公平鎖的“插隊”操作,前面講過CAS操作預設是原子性的,這樣就保證了設定的執行緒安全性。這是非公平鎖和公平鎖的第一點區別。

那麼這個state狀態是做什麼用的呢?從0設定到1又代表了什麼呢?再來看看跟state有關的原始碼:

protected final boolean compareAndSetState(int expect, int update) {
    // See below for intrinsics setup to support this
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

/**
    * The synchronization state.
    */
private volatile int state;

protected final int getState() {
    return state;
}

protected final void setState(int newState) {
    state = newState;
}
複製程式碼

首先state變數是一個volatile修飾的int型別變數,這樣就保證了這個變數在多執行緒環境下的可見性。從變數的註釋“The synchronization state”可以看出state代表了一個同步狀態。再回到上面的lock()方法,在設定成功之後,呼叫了setExclusiveOwnerThread方法將當前執行緒設定給了一個私有的變數,這個變數代表了當前獲取鎖的執行緒,放到了AQS的父類AbstractOwnableSynchronizer類中實現。

public abstract class AbstractOwnableSynchronizer
    implements java.io.Serializable {
    ......

    /**
     * The current owner of exclusive mode synchronization.
     */
    private transient Thread exclusiveOwnerThread;

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }

    protected final Thread getExclusiveOwnerThread() {
        return exclusiveOwnerThread;
    }
}
複製程式碼

如果設定state成功,lock()方法執行完畢,代表獲取了鎖。可以看出state狀態就是用來管理是否獲取到鎖的一個同步狀態,0代表鎖空閒,1代表獲取到了鎖。那麼如果設定state狀態不成功呢?接下來會呼叫acquire(1)方法,公平鎖則直接呼叫acquire(1)方法,不會用CAS操作進行插隊。acquire(1)方法是實現在AQS中的一個方法,看下他的原始碼:

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

這個方法很重要也很簡單理解,有幾步操作,首先呼叫tryAcquire嘗試獲取鎖,如果成功,則執行完畢,如果獲取失敗,則呼叫addWaiter方法新增當前執行緒到等待佇列,同時新增後執行acquireQueued方法掛起執行緒。如果掛起等待中需要中斷則執行selfInterrupt將執行緒中斷。下面來具體看看這個流程執行的細節,首先看看tryAcquire方法:

protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

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

//NonfairSync
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) { //鎖空閒
        if (compareAndSetState(0, acquires)) { //再次cas操作獲取鎖
            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;
}

//FairSync
protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() && //判斷佇列中是否已經存在等待執行緒,如果存在則獲取鎖失敗,需要排隊
            compareAndSetState(0, acquires)) { //不存在等待執行緒,再次cas操作獲取鎖
            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;
}

//AQS中實現,判斷佇列中是否已經存在等待執行緒
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());
}
複製程式碼

AQS沒有提供具體的實現,ReentrantLock中公平鎖和非公平鎖分別有自己的實現。非公平鎖在鎖空閒的狀態下再次CAS操作嘗試獲取鎖,保證執行緒安全。如果當前鎖非空閒,也就是state狀態不為0,則判斷是否是重入鎖,也就是同一個執行緒多次獲取鎖,是重入鎖則將state狀態+1,這也是ReentrantLock`支援鎖重入的邏輯。

公平鎖和非公平鎖在這上面有第二點區別,公平鎖在鎖空閒時首先會呼叫hasQueuedPredecessors方法判斷鎖等待佇列中是否存在等待執行緒,如果存在,則不會去嘗試獲取鎖,而是走接下來的排隊流程。至此非公平鎖和公平鎖的區別大家應該清楚了。如果面試時問道公平鎖和非公平鎖的區別,相信大家可以很容易答出來了。

通過tryAcquire獲取鎖失敗之後,會呼叫acquireQueued(addWaiter),先來看看addWaiter方法:

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);   //用EXCLUSIVE模式初始化一個Node節點,代表是一個獨佔鎖節點
    // 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)) { //cas設定尾節點為當前節點,將當前執行緒加入到佇列末尾,避免多執行緒設定導致資料丟失
            pred.next = node;
            return node;
        }
    }
    enq(node); //如果佇列中無等待執行緒,或者設定尾節點不成功,則迴圈設定尾節點
    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)) { //重複addWaiter中的設定尾節點,也是cas的經典操作--自旋,避免使用Synchronized關鍵字導致的執行緒掛起
                t.next = node;
                return t;
            }
        }
    }
}

static final class Node {
    /** Marker to indicate a node is waiting in shared mode */
    static final Node SHARED = new Node(); //共享模式
    /** Marker to indicate a node is waiting in exclusive mode */
    static final Node EXCLUSIVE = null;  //獨佔模式

    ......
}
複製程式碼

addWaiter方法首先初始化了一個EXCLUSIVE模式的Node節點。Node節點大家應該很熟悉,我寫的集合系列文章裡面介紹了很多鏈式結構都是通過這種方式來實現的。AQS中的Node也不例外,他的佇列結構也是通過實現一個Node內部類來實現的,這裡實現的是一個雙向佇列。Node節點分兩種模式,一種SHARED共享鎖模式,一種EXCLUSIVE獨佔鎖模式,ReentrantLock使用的是EXCLUSIVE獨佔鎖模式,所用用EXCLUSIVE來初始化。共享鎖模式後面的文章我們再詳細講解。

初始化Node節點之後就是將節點加入到佇列之中,這裡有一點要注意的是多執行緒環境下,如果CAS設定尾節點不成功,需要自旋進行CAS操作來設定尾節點,這樣即保證了執行緒安全,又保證了設定成功,這是一種樂觀的鎖模式,當然你可以通過synchronized關鍵字鎖住這個方法,但這樣效率就會下降,是一種悲觀鎖模式。

設定節點的過程我通過下面幾張圖來描述下,讓大家有更形象的理解:

Java併發(5)- ReentrantLock與AQS

Java併發(5)- ReentrantLock與AQS

Java併發(5)- ReentrantLock與AQS

將當前執行緒加入等待佇列之後,需要呼叫acquireQueued掛起當前執行緒:

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)) { //如果前置節點是頭節點,說明當前節點是第一個掛起的執行緒節點,再次cas嘗試獲取鎖
                setHead(node); //獲取鎖成功設定當前節點為頭節點,當前節點佔有鎖
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) && //非頭節點或者獲取鎖失敗,檢查節點狀態,檢視是否需要掛起執行緒
                parkAndCheckInterrupt())  //掛起執行緒,當前執行緒阻塞在這裡!
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}
複製程式碼

可以看到這個方法是一個自旋的過程,首先獲取當前節點的前置節點,如果前置節點為頭結點則再次嘗試獲取鎖,失敗則掛起阻塞,阻塞被取消後自旋這一過程。是否可以阻塞通過shouldParkAfterFailedAcquire方法來判斷,阻塞通過parkAndCheckInterrupt方法來執行。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL) //代表繼任節點需要掛起
        /*
            * This node has already set status asking a release
            * to signal it, so it can safely park.
            */
        return true;
    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 { //非可掛起狀態或退出狀態則嘗試設定為Node.SIGNAL狀態
        /*
            * 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.
            */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);//掛起當前執行緒
    return Thread.interrupted();
}
複製程式碼

只有當節點處於SIGNAL狀態時才可以掛起執行緒,Node的waitStatus有4個狀態分別是:

/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED =  1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL    = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
    * waitStatus value to indicate the next acquireShared should
    * unconditionally propagate
    */
static final int PROPAGATE = -3;
複製程式碼

註釋寫的很清楚,這裡就不詳細解釋著四種狀態了。到這裡整個Lock的過程我們就全部說完了,公平鎖和非公平鎖的區別從Lock的過程中我們也很容易發現,非公平鎖一樣要進行排隊,只不過在排隊之前會CAS嘗試直接獲取鎖。說完了獲取鎖,下面來看下釋放鎖的過程。

ReentrantLock的unLock()方法

unLock()方法比較好理解,因為他不需要考慮多執行緒的問題,如果unLock()的不是之前lock的執行緒,直接退出就可以了。看看unLock()的原始碼:

public class ReentrantLock implements Lock, java.io.Serializable {
    ......
    public void unlock() {
        sync.release(1);
    }
    ......
}

public abstract class AbstractQueuedSynchronizer {
    ......
    public final boolean release(int arg) {
        if (tryRelease(arg)) { //嘗試釋放鎖
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h); //釋放鎖成功後啟動後繼執行緒
            return true;
        }
        return false;
    }
    ......
}

abstract static class Sync extends AbstractQueuedSynchronizer {
    ......
    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread()) //釋放鎖必須要是獲取鎖的執行緒,否則退出,保證了這個方法只能單執行緒訪問
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) { //獨佔鎖為0後代表鎖釋放,否則為重入鎖,不釋放
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }
    ......
}

abstract static class Sync extends AbstractQueuedSynchronizer {
    ......
    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()方法一樣,會呼叫AQS的release方法,首先呼叫tryRelease嘗試釋放,首先必須要是當前獲取鎖的執行緒,之後判斷是否為重入鎖,非重入鎖則釋放當前執行緒的鎖。鎖釋放之後呼叫unparkSuccessor方法啟動後繼執行緒。

總結

ReentrantLock的獲取鎖和釋放鎖到這裡就講完了,總的來說還是比較清晰的一個流程,通過AQS的state狀態來控制鎖獲取和釋放狀態,AQS內部用一個雙向連結串列來維護掛起的執行緒。在AQS和ReentrantLock之間通過狀態和行為來分離,AQS用管理各種狀態,並內部通過連結串列管理執行緒佇列,ReentrantLock則對外提供鎖獲取和釋放的功能,具體實現則在AQS中。下面我通過兩張流程圖總結了公平鎖和非公平鎖的流程。

非公平鎖:

Java併發(5)- ReentrantLock與AQS
公平鎖:
Java併發(5)- ReentrantLock與AQS

相關文章