【洞悉AQS】通過ReentrantLock一步一圖徹底瞭解AQS實現原理

前言

談到併發,我們不得不說AQS(AbstractQueuedSynchronizer),所謂的AQS即是抽象的隊列式的同步器,內部定義了很多鎖相關的方法,例如:

  • getState():獲取鎖的標誌state值
  • setState():設置鎖的標誌state值
  • tryAcquire(int):獨佔方式獲取鎖。嘗試獲取資源,成功則返回true,失敗則返回false。
  • tryRelease(int):獨佔方式釋放鎖。嘗試釋放資源,成功則返回true,失敗則返回false。

這裏還有更多的方法並沒有列出來,我們以ReentrantLock作爲突破點通過源碼和畫圖的形式一步步瞭解AQS內部實現原理。

image.png

目錄結構

文章準備模擬場景來進行解析:

三個線程(線程一、線程二、線程三)同時來加鎖/釋放鎖,然後通過代碼和畫圖一步步解析其中的實現。

目錄如下:

  • 線程一加鎖成功時AQS內部實現
  • 線程二/三加鎖失敗時AQS中等待隊列的數據模型
  • 線程一釋放鎖及線程二獲取鎖實現原理

這裏會分析每個線程加鎖、釋放鎖內部的一系列實現原理

AQS實現原理

AQS中 維護了一個volatile int state(代表共享資源)和一個FIFO線程等待隊列(多線程爭用資源被阻塞時會進入此隊列)。

這裏volatile能夠保證多線程下的可見性,當state=1則代表當前對象鎖已經被佔有,其他線程來加鎖時則會失敗,然後線程放入一個FIFO的等待隊列中使用UNSAFE.park()來掛起當前線程。

另外state的操作都是使用CAS來保證其併發修改的安全性。

具體原理我們可以用一張圖來簡單概括:(此圖片來源:https://www.cnblogs.com/waterystone/p/4920797.html)

image.pngimage.png

場景分析

線程一加鎖成功

如果同時有三個線程併發搶佔鎖,此時線程一搶佔鎖成功:

image.png

此時線程二線程三加鎖失敗:

image.png

搶佔鎖代碼實現:

java.util.concurrent.locks.ReentrantLock .NonfairSync:

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

這裏使用的ReentrantLock非公平鎖,線程進來直接利用CAS嘗試搶佔鎖,如果搶佔成功則state被改爲1,然後設置獨佔鎖線程爲當前線程。如下所示:

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

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

線程二搶佔鎖失敗

我們按照真實場景來分析,此時線程一搶佔鎖成功,state變爲1,所以線程二通過CAS修改state變量必然會失敗。此時AQSFIFO隊列中數據如圖所示:

image.png

我們將線程二執行的邏輯一步步拆解來看:

java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire():

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

先看看tryAcquire()的具體實現:
java.util.concurrent.locks.ReentrantLock .nonfairTryAcquire():

final boolean nonfairTryAcquire(int acquires) {
    // 獲取當前線程
    final Thread current = Thread.currentThread();
    // 獲取state狀態,線程一加鎖成功了,此時state=1
    int c = getState();
    // 如果state=0,說明可以嘗試利用CAS進行加鎖操作
    if (c == 0) {
        // 加鎖成功的邏輯
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // 如果當前線程和獨佔鎖線程是同一個,那麼可以重入加鎖
    // 重入加鎖後,state=2
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    // 返回false加鎖失敗
    return false;
}

這段代碼走下來,此時全局變量state=1,所以通過CAS修改state的值不會成功。

而此時持有鎖的線程是線程一,所以線程二*也不滿足重入的條件。

線程二執行tryAcquire()後返回false,接着執行addWaiter(Node.EXCLUSIVE),代碼實現如下:

java.util.concurrent.locks.AbstractQueuedSynchronizer.addWaiter():

private Node addWaiter(Node mode) {
    // mode = Node EXCLUSIVE = null
    // 創建一個新的node,thread = 線程二,nextWaiter = null
    Node node = new Node(Thread.currentThread(), mode);
    // 此時tail = null,直接執行enq操作
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 線程二直接進入隊列操作
    enq(node);
    return node;
}

此時tail指針爲空,直接調用enq(node)將當前線程加入等待隊列尾部:

private Node enq(final Node node) {
    for (;;) {
        // 第一次循環tail = null
        Node t = tail;
        if (t == null) {
            // 用CAS將head設置爲一個新Node
            if (compareAndSetHead(new Node()))
                // tail 和 head都指向這個新Node
                tail = head;
        } else {
            // 第二次循環進入,node的前置節點設置爲tail = head
            node.prev = t;
            // 用CAS操作設置tail節點爲當前傳入的node節點
            if (compareAndSetTail(t, node)) {
                // t開始時指向tail=head節點,通過CAS將tail指向node後
                // 設置t.next=head.next=node
                t.next = node;
                // 返回t=head節點
                return t;
            }
        }
    }
}

第一遍循環tail指針爲空,進入if邏輯中,此時隊列中數據:

image.png

執行完成之後,headtailt都指向第一個元素(new Node())

接着執行第二遍循環,進入else邏輯,此時已經有了head節點,這裏要操作的就是將線程二這個Node節點掛到head節點上來。

addWaiter()方法執行完後會返回當前插入線程二構建的Node節點,此時隊列中的數據爲:

image.png

再接着看看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
執行邏輯,此時傳入的爲線程二構建的Node信息:

java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued():

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // p爲線程二Node的前置節點,也就是head節點
            final Node p = node.predecessor();
            // p= head成立,繼續使用CAS嘗試加鎖,此時線程一還在持有鎖
            // state = 1,所以加鎖失敗
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 執行shouldParkAfterFailedAcquire方法
            // shouldParkAfterFailedAcquire方法中將head節點的waitStatus變爲了SIGNAL=-1
            // 接着執行parkAndChecknIterrupt,調用LockSupport.park()
            // 掛起當前線程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndChecknIterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // ws爲head節點的waitStatus=null
    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 {
        // 通過CAS操作,將head節點的waitStatus變爲SIGNAL=-1
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

private final boolean parkAndCheckInterrupt() {
    // 調用底層的park方法,掛起當前線程
    LockSupport.park(this);
    return Thread.interrupted();
}

acquireQueued()這個方法會先判斷當前傳入的Node對應的前置節點是否爲head,如果是則嘗試加鎖。加鎖成功過則將當前節點設置爲head節點,然後空置之前的head節點。

如果加鎖失敗或者Node的前置節點不是head節點,首先將Node的前置節點中的waitStatus設置爲SIGNAL(值爲-1),
然後掛起當前Node節點(當前Node線程二創建的節點),操作後AQS隊列中的數據如下圖:

image.png

此時線程二就靜靜的待在AQS的等待隊列裏面了,等着其他線程釋放鎖來喚醒掛起的線程。

線程三搶佔鎖失敗

看完了線程二搶佔鎖失敗的分析,那麼再來分析線程三搶佔鎖失敗就很簡單了,先看看addWaiter(Node mode)方法:

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // pred = tail = 線程二
    Node pred = tail;
    // 此時pred不爲空
    if (pred != null) {
        // 設置線程三的前置節點爲線程二
        node.prev = pred;
        // 使用CAS將線程三設置爲tail節點
        if (compareAndSetTail(pred, node)) {
            // pred = 線程二的next節點設置爲線程三
            pred.next = node;
            // 返回線程三節點
            return node;
        }
    }
    enq(node);
    return node;
}

執行完後AQS中隊列數據如圖:

image.png

接着執行acquireQueued()方法:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // p = 線程三的前置節點= 線程二
            final Node p = node.predecessor();
            // 判斷線程二是否是head節點,如果是則嘗試搶佔鎖
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 執行shouldParkAfterFailedAcquire方法
            // 將線程三的前置節點線程二中的waitStatus變爲SIGNAL = -1
            // 然後執行parkAndCheckInterrupt將線程三也掛起
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

執行完後AQS中隊列數據如圖:

image.png

線程一釋放鎖

現在來分析下釋放鎖的過程,首先是線程一釋放鎖,釋放鎖後會喚醒head節點的後置節點,也就是我們現在的線程二,執行完後AQS隊列數據如下:

image.png

此時線程二已經被喚醒,繼續嘗試獲取鎖,如果獲取鎖失敗,則會繼續被掛起。如果獲取鎖成功,則AQS中數據如圖:

image.png

接着還是一步步拆解來看,先看看線程一釋放鎖的代碼:

java.util.concurrent.locks.AbstractQueuedSynchronizer.release()

public final boolean release(int arg) {
    // tryRelease() 方法實現在ReentrantLock中實現的
    if (tryRelease(arg)) {
        // 如果釋放鎖成功,定義h=head節點
        Node h = head;
        // 如果head不爲空,此時head.waitStatus=SIGNAL=-1
        if (h != null && h.waitStatus != 0)
            // 執行喚醒操作,喚醒之前掛起的線程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

此時看ReentrantLock.tryRelease()中的具體實現:

protected final boolean tryRelease(int releases) {
    // getState()是獲取state變量,此時state=1
    // releases傳入進來的也是1,所以c=0
    int c = getState() - releases;
    // 如果釋放鎖的線程,不是當前獨佔鎖線程,直接拋異常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 如果c=0則說明釋放鎖成功
    if (c == 0) {
        free = true;
        // 設置獨佔鎖線程爲null
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

執行完ReentrantLock.tryRelease()後,state被設置成0,Lock對象的獨佔鎖被設置爲null。可以看下執行後AQS中的數據:

image.png

接着執行java.util.concurrent.locks.AbstractQueuedSynchronizer.unparkSuccessor()方法,喚醒head的後置節點:

private void unparkSuccessor(Node node) {
    // node爲head線程,此時node.waitStatus=SIGNAL=-1
    int ws = node.waitStatus;
    if (ws < 0)
        // 設置node節點的waitStatus爲0
        compareAndSetWaitStatus(node, ws, 0);

    // s=node.next=head.next,獲取線程二Node節點
    Node s = node.next;
    // 此時s.waitStatus=SIGNAL=-1 條件不成立
    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;
    }
    // 執行LockSupport.unpark,這裏是喚醒線程二
    if (s != null)
        LockSupport.unpark(s.thread);
}

邏輯如圖:

image.png

此時線程二被喚醒,線程二接着之前被park的地方繼續執行,繼續執行acquireQueued()方法。

線程二喚醒繼續加鎖

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 獲取線程二的前置節點爲head
            final Node p = node.predecessor();
            // p=head,然後嘗試加鎖,對state進行CAS操作
            // 此時仍有可能加鎖失敗,因爲我們使用的ReentrantLock中的非公平鎖
            // 如果真好有新的線程進來掙錢鎖,線程二就有可能加鎖失敗
            // 如果加鎖失敗就還會被掛起
            if (p == head && tryAcquire(arg)) {
                // 線程二加鎖成功,將線程二設置爲head節點
                setHead(node);
                // 設置p.next=head.next爲null,方便GC
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

執行完後線程二獲取鎖,而線程二也變爲head節點,之前的head節點被空置,等着被垃圾回收,此時AQS中隊列數據如下:

image.png

此時 線程二獲取鎖成功,並且之前的head節點會被垃圾回收掉。

線程二釋放鎖/線程三加鎖

線程二釋放鎖時,會喚醒被掛起的線程三,流程和上面大致相同,被喚醒的線程三會再次嘗試加鎖,具體代碼就不再分析了,此時AQS中隊列數據如圖:

image.png

總結

這裏用了一步一圖的方式來展示了ReentrantLock的實現方式,而ReentrantLock底層就是基於AQS實現的,所以我們也對AQS有了深刻的理解。

由於篇幅原因,還有很多細節沒有講到,比如ReentrantLock公平鎖的實現,可重入鎖的實現,Condition的實現等,當然大家可以依照着我這種分析模式去嘗試一步步畫圖解析,相信這些細節也都能一步步破解。

後面我還會介紹ReentrantReadWriteLock
的實現原理,仍然使用一步一圖的模式來講解,敬請期待。

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