Java并發(fā)指南7:JUC的核心類AQS詳解

本文轉(zhuǎn)自:https://www.javadoop.com/post/AbstractQueuedSynchronizer#toc4

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫漠嵌,更多精彩內(nèi)容請到我的倉庫里查看

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點下Star哈

文章同步發(fā)于我的個人博客:

www.how2playlife.com

本文是微信公眾號【Java技術(shù)江湖】的《Java并發(fā)指南》其中一篇弓摘,本文大部分內(nèi)容來源于網(wǎng)絡(luò)触幼,為了把本文主題講得清晰透徹,也整合了很多我認為不錯的技術(shù)博客內(nèi)容瞳抓,引用其中了一些比較好的博客文章,如有侵權(quán)伏恐,請聯(lián)系作者孩哑。

該系列博文會告訴你如何全面深入地學習Java并發(fā)技術(shù),從Java多線程基礎(chǔ)翠桦,再到并發(fā)編程的基礎(chǔ)知識臭笆,從Java并發(fā)包的入門和實戰(zhàn),再到JUC的源碼剖析,一步步地學習Java并發(fā)編程愁铺,并上手進行實戰(zhàn)鹰霍,以便讓你更完整地了解整個Java并發(fā)編程知識體系,形成自己的知識框架茵乱。

為了更好地總結(jié)和檢驗?zāi)愕膶W習成果茂洒,本系列文章也會提供一些對應(yīng)的面試題以及參考答案。

如果對本系列文章有什么建議瓶竭,或者是有什么疑問的話督勺,也可以關(guān)注公眾號【Java技術(shù)江湖】聯(lián)系作者,歡迎你參與本系列博文的創(chuàng)作和修訂斤贰。

簡介

在分析 Java 并發(fā)包 java.util.concurrent 源碼的時候智哀,少不了需要了解 AbstractQueuedSynchronizer(以下簡寫AQS)這個抽象類,因為它是 Java 并發(fā)包的基礎(chǔ)工具類荧恍,是實現(xiàn) ReentrantLock瓷叫、CountDownLatch、Semaphore送巡、FutureTask 等類的基礎(chǔ)摹菠。

Google 一下 AbstractQueuedSynchronizer,我們可以找到很多關(guān)于 AQS 的介紹骗爆,但是很多都沒有介紹清楚次氨,因為大部分文章沒有把其中的一些關(guān)鍵的細節(jié)說清楚。

本文將從 ReentrantLock 的公平鎖源碼出發(fā)摘投,分析下 AbstractQueuedSynchronizer 這個類是怎么工作的煮寡,希望能給大家提供一些簡單的幫助。

申明以下幾點:

  1. 本文有點長犀呼,但還是挺簡單洲押,主要面向讀者對象為并發(fā)編程的初學者,或者想要閱讀 Java 并發(fā)包源碼的開發(fā)者圆凰。對于新手來說杈帐,可能需要花好幾個小時才能完全看懂,但是這時間肯定是值得的专钉。
  2. 源碼環(huán)境 JDK1.7(1.8沒啥變化)挑童,看到不懂或有疑惑的部分,最好能自己打開源碼看看跃须。Doug Lea 大神的代碼寫得真心不錯站叼。
  3. 本文不分析共享模式,這樣可以給讀者減少很多負擔菇民,第三篇文章對共享模式進行了分析尽楔。而且也不分析 condition 部分投储,所以應(yīng)該說很容易就可以看懂了。
  4. 本文大量使用我們平時用得最多的 ReentrantLock 的概念阔馋,本質(zhì)上來說是不正確的玛荞,讀者應(yīng)該清楚,AQS 不僅僅用來實現(xiàn)可重入鎖呕寝,只是希望讀者可以用鎖來聯(lián)想 AQS 的使用場景勋眯,降低閱讀壓力。
  5. ReentrantLock 的公平鎖和非公平鎖只有一點點區(qū)別下梢,第二篇文章做了介紹客蹋。
  6. 評論區(qū)有讀者反饋本文直接用代碼說不友好,應(yīng)該多配點流程圖孽江,這篇文章確實有這個問題讶坯。但是作為過來人,我想告訴大家岗屏,對于 AQS 來說辆琅,形式真的不重要,重要的是把細節(jié)說清楚担汤。

AQS 結(jié)構(gòu)

先來看看 AQS 有哪些屬性涎跨,搞清楚這些基本就知道 AQS 是什么套路了洼冻,畢竟可以猜嘛崭歧!

// 頭結(jié)點,你直接把它當做 當前持有鎖的線程 可能是最好理解的
private transient volatile Node head;

// 阻塞的尾節(jié)點撞牢,每個新的節(jié)點進來率碾,都插入到最后,也就形成了一個鏈表
private transient volatile Node tail;

// 這個是最重要的屋彪,代表當前鎖的狀態(tài)所宰,0代表沒有被占用,大于 0 代表有線程持有當前鎖
// 這個值可以大于 1畜挥,是因為鎖可以重入仔粥,每次重入都加上 1
private volatile int state;

// 代表當前持有獨占鎖的線程,舉個最重要的使用例子蟹但,因為鎖可以重入
// reentrantLock.lock()可以嵌套調(diào)用多次躯泰,所以每次用這個來判斷當前線程是否已經(jīng)擁有了鎖
// if (currentThread == getExclusiveOwnerThread()) {state++}
private transient Thread exclusiveOwnerThread; //繼承自AbstractOwnableSynchronizer

怎么樣,看樣子應(yīng)該是很簡單的吧华糖,畢竟也就四個屬性啊麦向。

AbstractQueuedSynchronizer 的等待隊列示意如下所示,注意了客叉,之后分析過程中所說的 queue诵竭,也就是阻塞隊列不包含 head话告,不包含 head,不包含 head卵慰。

aqs-0

等待隊列中每個線程被包裝成一個 Node 實例沙郭,數(shù)據(jù)結(jié)構(gòu)是鏈表,一起看看源碼吧:

static final class Node {
    // 標識節(jié)點當前在共享模式下
    static final Node SHARED = new Node();
    // 標識節(jié)點當前在獨占模式下
    static final Node EXCLUSIVE = null;

    // ======== 下面的幾個int常量是給waitStatus用的 ===========
    /** waitStatus value to indicate thread has cancelled */
    // 代碼此線程取消了爭搶這個鎖
    static final int CANCELLED =  1;
    /** waitStatus value to indicate successor's thread needs unparking */
    // 官方的描述是呵燕,其表示當前node的后繼節(jié)點對應(yīng)的線程需要被喚醒
    static final int SIGNAL    = -1;
    /** waitStatus value to indicate thread is waiting on condition */
    // 本文不分析condition棠绘,所以略過吧,下一篇文章會介紹這個
    static final int CONDITION = -2;
    /**
     * waitStatus value to indicate the next acquireShared should
     * unconditionally propagate
     */
    // 同樣的不分析再扭,略過吧
    static final int PROPAGATE = -3;
    // =====================================================

    // 取值為上面的1氧苍、-1、-2泛范、-3让虐,或者0(以后會講到)
    // 這么理解,暫時只需要知道如果這個值 大于0 代表此線程取消了等待罢荡,
    //    ps: 半天搶不到鎖赡突,不搶了,ReentrantLock是可以指定timeouot的区赵。惭缰。。
    volatile int waitStatus;
    // 前驅(qū)節(jié)點的引用
    volatile Node prev;
    // 后繼節(jié)點的引用
    volatile Node next;
    // 這個就是線程本尊
    volatile Thread thread;

}

Node 的數(shù)據(jù)結(jié)構(gòu)其實也挺簡單的笼才,就是 thread + waitStatus + pre + next 四個屬性而已漱受,大家先要有這個概念在心里。

上面的是基礎(chǔ)知識骡送,后面會多次用到昂羡,心里要時刻記著它們,心里想著這個結(jié)構(gòu)圖就可以了摔踱。下面虐先,我們開始說 ReentrantLock 的公平鎖。再次強調(diào)派敷,我說的阻塞隊列不包含 head 節(jié)點蛹批。

aqs-0

首先,我們先看下 ReentrantLock 的使用方式篮愉。

// 我用個web開發(fā)中的service概念吧
public class OrderService {
    // 使用static腐芍,這樣每個線程拿到的是同一把鎖,當然潜支,spring mvc中service默認就是單例甸赃,別糾結(jié)這個
    private static ReentrantLock reentrantLock = new ReentrantLock(true);

    public void createOrder() {
        // 比如我們同一時間,只允許一個線程創(chuàng)建訂單
        reentrantLock.lock();
        // 通常冗酿,lock 之后緊跟著 try 語句
        try {
            // 這塊代碼同一時間只能有一個線程進來(獲取到鎖的線程)埠对,
            // 其他的線程在lock()方法上阻塞络断,等待獲取到鎖,再進來
            // 執(zhí)行代碼...
            // 執(zhí)行代碼...
            // 執(zhí)行代碼...
        } finally {
            // 釋放鎖
            reentrantLock.unlock();
        }
    }
}

ReentrantLock 在內(nèi)部用了內(nèi)部類 Sync 來管理鎖项玛,所以真正的獲取鎖和釋放鎖是由 Sync 的實現(xiàn)類來控制的貌笨。

abstract static class Sync extends AbstractQueuedSynchronizer {
}

Sync 有兩個實現(xiàn),分別為 NonfairSync(非公平鎖)和 FairSync(公平鎖)襟沮,我們看 FairSync 部分锥惋。

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

線程搶鎖

很多人肯定開始嫌棄上面廢話太多了,下面跟著代碼走开伏,我就不廢話了膀跌。

static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;
      // 爭鎖
    final void lock() {
        acquire(1);
    }
      // 來自父類AQS,我直接貼過來這邊固灵,下面分析的時候同樣會這樣做捅伤,不會給讀者帶來閱讀壓力
    // 我們看到,這個方法巫玻,如果tryAcquire(arg) 返回true, 也就結(jié)束了丛忆。
    // 否則,acquireQueued方法會將線程壓到隊列中
    public final void acquire(int arg) { // 此時 arg == 1
        // 首先調(diào)用tryAcquire(1)一下仍秤,名字上就知道熄诡,這個只是試一試
        // 因為有可能直接就成功了呢,也就不需要進隊列排隊了诗力,
        // 對于公平鎖的語義就是:本來就沒人持有鎖凰浮,根本沒必要進隊列等待(又是掛起,又是等待被喚醒的)
        if (!tryAcquire(arg) &&
            // tryAcquire(arg)沒有成功姜骡,這個時候需要把當前線程掛起导坟,放到阻塞隊列中屿良。
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
              selfInterrupt();
        }
    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    // 嘗試直接獲取鎖圈澈,返回值是boolean,代表是否獲取到鎖
    // 返回true:1.沒有線程在等待鎖尘惧;2.重入鎖康栈,線程本來就持有鎖,也就可以理所當然可以直接獲取
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        // state == 0 此時此刻沒有線程持有鎖
        if (c == 0) {
            // 雖然此時此刻鎖是可以用的喷橙,但是這是公平鎖啥么,既然是公平,就得講究先來后到贰逾,
            // 看看有沒有別人在隊列中等了半天了
            if (!hasQueuedPredecessors() &&
                // 如果沒有線程在等待悬荣,那就用CAS嘗試一下,成功了就獲取到鎖了疙剑,
                // 不成功的話氯迂,只能說明一個問題践叠,就在剛剛幾乎同一時刻有個線程搶先了 =_=
                // 因為剛剛還沒人的,我判斷過了
                compareAndSetState(0, acquires)) {

                // 到這里就是獲取到鎖了嚼蚀,標記一下禁灼,告訴大家,現(xiàn)在是我占用了鎖
                setExclusiveOwnerThread(current);
                return true;
            }
        }
          // 會進入這個else if分支轿曙,說明是重入了弄捕,需要操作:state=state+1
        // 這里不存在并發(fā)問題
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        // 如果到這里,說明前面的if和else if都沒有返回true导帝,說明沒有獲取到鎖
        // 回到上面一個外層調(diào)用方法繼續(xù)看:
        // if (!tryAcquire(arg) 
        //        && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 
        //     selfInterrupt();
        return false;
    }

    // 假設(shè)tryAcquire(arg) 返回false守谓,那么代碼將執(zhí)行:
      //        acquireQueued(addWaiter(Node.EXCLUSIVE), arg),
    // 這個方法您单,首先需要執(zhí)行:addWaiter(Node.EXCLUSIVE)

    /**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    // 此方法的作用是把線程包裝成node分飞,同時進入到隊列中
    // 參數(shù)mode此時是Node.EXCLUSIVE,代表獨占模式
    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加到鏈表的最后面去睹限,也就是進到阻塞隊列的最后
        Node pred = tail;

        // tail!=null => 隊列不為空(tail==head的時候譬猫,其實隊列是空的,不過不管這個吧)
        if (pred != null) { 
            // 將當前的隊尾節(jié)點羡疗,設(shè)置為自己的前驅(qū) 
            node.prev = pred; 
            // 用CAS把自己設(shè)置為隊尾, 如果成功后染服,tail == node 了,這個節(jié)點成為阻塞隊列新的尾巴
            if (compareAndSetTail(pred, node)) { 
                // 進到這里說明設(shè)置成功叨恨,當前node==tail, 將自己與之前的隊尾相連柳刮,
                // 上面已經(jīng)有 node.prev = pred,加上下面這句痒钝,也就實現(xiàn)了和之前的尾節(jié)點雙向連接了
                pred.next = node;
                // 線程入隊了秉颗,可以返回了
                return node;
            }
        }
        // 仔細看看上面的代碼,如果會到這里送矩,
        // 說明 pred==null(隊列是空的) 或者 CAS失敗(有線程在競爭入隊)
        // 讀者一定要跟上思路蚕甥,如果沒有跟上,建議先不要往下讀了栋荸,往回仔細看菇怀,否則會浪費時間的
        enq(node);
        return node;
    }

    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    // 采用自旋的方式入隊
    // 之前說過,到這個方法只有兩種可能:等待隊列為空晌块,或者有線程競爭入隊爱沟,
    // 自旋在這邊的語義是:CAS設(shè)置tail過程中,競爭一次競爭不到匆背,我就多次競爭呼伸,總會排到的
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            // 之前說過,隊列為空也會進來這里
            if (t == null) { // Must initialize
                // 初始化head節(jié)點
                // 細心的讀者會知道原來 head 和 tail 初始化的時候都是 null 的
                // 還是一步CAS钝尸,你懂的括享,現(xiàn)在可能是很多線程同時進來呢
                if (compareAndSetHead(new Node()))
                    // 給后面用:這個時候head節(jié)點的waitStatus==0, 看new Node()構(gòu)造方法就知道了

                    // 這個時候有了head闽铐,但是tail還是null,設(shè)置一下奶浦,
                    // 把tail指向head兄墅,放心,馬上就有線程要來了澳叉,到時候tail就要被搶了
                    // 注意:這里只是設(shè)置了tail=head隙咸,這里可沒return哦,沒有return成洗,沒有return
                    // 所以五督,設(shè)置完了以后,繼續(xù)for循環(huán)瓶殃,下次就到下面的else分支了
                    tail = head;
            } else {
                // 下面幾行充包,和上一個方法 addWaiter 是一樣的,
                // 只是這個套在無限循環(huán)里遥椿,反正就是將當前線程排到隊尾基矮,有線程競爭的話排不上重復(fù)排
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

    // 現(xiàn)在,又回到這段代碼了
    // if (!tryAcquire(arg) 
    //        && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 
    //     selfInterrupt();

    // 下面這個方法冠场,參數(shù)node家浇,經(jīng)過addWaiter(Node.EXCLUSIVE),此時已經(jīng)進入阻塞隊列
    // 注意一下:如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的話碴裙,
    // 意味著上面這段代碼將進入selfInterrupt()钢悲,所以正常情況下,下面應(yīng)該返回false
    // 這個方法非常重要舔株,應(yīng)該說真正的線程掛起莺琳,然后被喚醒后去獲取鎖,都在這個方法里了
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                // p == head 說明當前節(jié)點雖然進到了阻塞隊列载慈,但是是阻塞隊列的第一個惭等,因為它的前驅(qū)是head
                // 注意,阻塞隊列不包含head節(jié)點娃肿,head一般指的是占有鎖的線程咕缎,head后面的才稱為阻塞隊列
                // 所以當前節(jié)點可以去試搶一下鎖
                // 這里我們說一下珠十,為什么可以去試試:
                // 首先料扰,它是隊頭,這個是第一個條件焙蹭,其次晒杈,當前的head有可能是剛剛初始化的node,
                // enq(node) 方法里面有提到孔厉,head是延時初始化的拯钻,而且new Node()的時候沒有設(shè)置任何線程
                // 也就是說帖努,當前的head不屬于任何一個線程,所以作為隊頭粪般,可以去試一試拼余,
                // tryAcquire已經(jīng)分析過了, 忘記了請往前看一下,就是簡單用CAS試操作一下state
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 到這里亩歹,說明上面的if分支沒有成功匙监,要么當前node本來就不是隊頭,
                // 要么就是tryAcquire(arg)沒有搶贏別人小作,繼續(xù)往下看
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            // 什么時候 failed 會為 true???
            // tryAcquire() 方法拋異常的情況
            if (failed)
                cancelAcquire(node);
        }
    }

    /**
     * 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
     */
    // 剛剛說過亭姥,會到這里就是沒有搶到鎖唄,這個方法說的是:"當前線程沒有搶到鎖顾稀,是否需要掛起當前線程达罗?"
    // 第一個參數(shù)是前驅(qū)節(jié)點,第二個參數(shù)才是代表當前線程的節(jié)點
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        // 前驅(qū)節(jié)點的 waitStatus == -1 静秆,說明前驅(qū)節(jié)點狀態(tài)正常粮揉,當前線程需要掛起,直接可以返回true
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;

        // 前驅(qū)節(jié)點 waitStatus大于0 抚笔,之前說過滔蝉,大于0 說明前驅(qū)節(jié)點取消了排隊。
        // 這里需要知道這點:進入阻塞隊列排隊的線程會被掛起塔沃,而喚醒的操作是由前驅(qū)節(jié)點完成的蝠引。
        // 所以下面這塊代碼說的是將當前節(jié)點的prev指向waitStatus<=0的節(jié)點,
        // 簡單說蛀柴,就是為了找個好爹螃概,因為你還得依賴它來喚醒呢,如果前驅(qū)節(jié)點取消了排隊鸽疾,
        // 找前驅(qū)節(jié)點的前驅(qū)節(jié)點做爹吊洼,往前遍歷總能找到一個好爹的
        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.
             */
            // 仔細想想,如果進入到這個分支意味著什么
            // 前驅(qū)節(jié)點的waitStatus不等于-1和1制肮,那也就是只可能是0冒窍,-2,-3
            // 在我們前面的源碼中豺鼻,都沒有看到有設(shè)置waitStatus的综液,所以每個新的node入隊時,waitStatu都是0
            // 正常情況下儒飒,前驅(qū)節(jié)點是之前的 tail谬莹,那么它的 waitStatus 應(yīng)該是 0
            // 用CAS將前驅(qū)節(jié)點的waitStatus設(shè)置為Node.SIGNAL(也就是-1)
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        // 這個方法返回 false,那么會再走一次 for 循序,
        //     然后再次進來此方法附帽,此時會從第一個分支返回 true
        return false;
    }

    // private static boolean shouldParkAfterFailedAcquire(Node pred, Node node)
    // 這個方法結(jié)束根據(jù)返回值我們簡單分析下:
    // 如果返回true, 說明前驅(qū)節(jié)點的waitStatus==-1埠戳,是正常情況,那么當前線程需要被掛起蕉扮,等待以后被喚醒
    //        我們也說過整胃,以后是被前驅(qū)節(jié)點喚醒,就等著前驅(qū)節(jié)點拿到鎖喳钟,然后釋放鎖的時候叫你好了
    // 如果返回false, 說明當前不需要被掛起爪模,為什么呢?往后看

    // 跳回到前面是這個方法
    // if (shouldParkAfterFailedAcquire(p, node) &&
    //                parkAndCheckInterrupt())
    //                interrupted = true;

    // 1\. 如果shouldParkAfterFailedAcquire(p, node)返回true荚藻,
    // 那么需要執(zhí)行parkAndCheckInterrupt():

    // 這個方法很簡單屋灌,因為前面返回true,所以需要掛起線程应狱,這個方法就是負責掛起線程的
    // 這里用了LockSupport.park(this)來掛起線程共郭,然后就停在這里了,等待被喚醒=======
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

    // 2\. 接下來說說如果shouldParkAfterFailedAcquire(p, node)返回false的情況

   // 仔細看shouldParkAfterFailedAcquire(p, node)疾呻,我們可以發(fā)現(xiàn)除嘹,其實第一次進來的時候,一般都不會返回true的岸蜗,原因很簡單尉咕,前驅(qū)節(jié)點的waitStatus=-1是依賴于后繼節(jié)點設(shè)置的。也就是說璃岳,我都還沒給前驅(qū)設(shè)置-1呢年缎,怎么可能是true呢,但是要看到铃慷,這個方法是套在循環(huán)里的单芜,所以第二次進來的時候狀態(tài)就是-1了。

    // 解釋下為什么shouldParkAfterFailedAcquire(p, node)返回false的時候不直接掛起線程:
    // => 是為了應(yīng)對在經(jīng)過這個方法后犁柜,node已經(jīng)是head的直接后繼節(jié)點了洲鸠。剩下的讀者自己想想吧。
}

說到這里馋缅,也就明白了扒腕,多看幾遍 final boolean acquireQueued(final Node node, int arg) 這個方法吧。自己推演下各個分支怎么走萤悴,哪種情況下會發(fā)生什么瘾腰,走到哪里。

解鎖操作

最后稚疹,就是還需要介紹下喚醒的動作了居灯。我們知道祭务,正常情況下内狗,如果線程沒獲取到鎖怪嫌,線程會被 LockSupport.park(this); 掛起停止,等待被喚醒柳沙。

// 喚醒的代碼還是比較簡單的岩灭,你如果上面加鎖的都看懂了,下面都不需要看就知道怎么回事了
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;
}

// 回到ReentrantLock看tryRelease方法
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    // 是否完全釋放鎖
    boolean free = false;
    // 其實就是重入的問題赂鲤,如果c==0噪径,也就是說沒有嵌套鎖了,可以釋放了数初,否則還不能釋放掉
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

/**
 * Wakes up node's successor, if one exists.
 *
 * @param node the node
 */
// 喚醒后繼節(jié)點
// 從上面調(diào)用處知道找爱,參數(shù)node是head頭結(jié)點
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;
    // 如果head節(jié)點當前waitStatus<0, 將其修改為0
    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.
     */
    // 下面的代碼就是喚醒后繼節(jié)點,但是有可能后繼節(jié)點取消了等待(waitStatus==1)
    // 從隊尾往前找泡孩,找到waitStatus<=0的所有節(jié)點中排在最前面的
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 從后往前找车摄,仔細看代碼,不必擔心中間有節(jié)點取消(waitStatus==1)的情況
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        // 喚醒線程
        LockSupport.unpark(s.thread);
}

喚醒線程以后仑鸥,被喚醒的線程將從以下代碼中繼續(xù)往前走:

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this); // 剛剛線程被掛起在這里了
    return Thread.interrupted();
}
// 又回到這個方法了:acquireQueued(final Node node, int arg)吮播,這個時候,node的前驅(qū)是head了

好了眼俊,后面就不分析源碼了意狠,剩下的還有問題自己去仔細看看代碼吧。

總結(jié)

總結(jié)一下吧疮胖。

在并發(fā)環(huán)境下环戈,加鎖和解鎖需要以下三個部件的協(xié)調(diào):

  1. 鎖狀態(tài)。我們要知道鎖是不是被別的線程占有了澎灸,這個就是 state 的作用谷市,它為 0 的時候代表沒有線程占有鎖,可以去爭搶這個鎖击孩,用 CAS 將 state 設(shè)為 1迫悠,如果 CAS 成功,說明搶到了鎖巩梢,這樣其他線程就搶不到了创泄,如果鎖重入的話,state進行 +1 就可以括蝠,解鎖就是減 1鞠抑,直到 state 又變?yōu)?0,代表釋放鎖忌警,所以 lock() 和 unlock() 必須要配對啊搁拙。然后喚醒等待隊列中的第一個線程秒梳,讓其來占有鎖。
  2. 線程的阻塞和解除阻塞箕速。AQS 中采用了 LockSupport.park(thread) 來掛起線程酪碘,用 unpark 來喚醒線程。
  3. 阻塞隊列盐茎。因為爭搶鎖的線程可能很多兴垦,但是只能有一個線程拿到鎖,其他的線程都必須等待字柠,這個時候就需要一個 queue 來管理這些線程探越,AQS 用的是一個 FIFO 的隊列,就是一個鏈表窑业,每個 node 都持有后繼節(jié)點的引用钦幔。AQS 采用了 CLH 鎖的變體來實現(xiàn),感興趣的讀者可以參考這篇文章關(guān)于CLH的介紹常柄,寫得簡單明了鲤氢。

示例圖解析

下面屬于回顧環(huán)節(jié),用簡單的示例來說一遍拐纱,如果上面的有些東西沒看懂铜异,這里還有一次幫助你理解的機會。

首先秸架,第一個線程調(diào)用 reentrantLock.lock()揍庄,翻到最前面可以發(fā)現(xiàn),tryAcquire(1) 直接就返回 true 了东抹,結(jié)束蚂子。只是設(shè)置了 state=1,連 head 都沒有初始化缭黔,更談不上什么阻塞隊列了食茎。要是線程 1 調(diào)用 unlock() 了,才有線程 2 來馏谨,那世界就太太太平了别渔,完全沒有交集嘛,那我還要 AQS 干嘛惧互。

如果線程 1 沒有調(diào)用 unlock() 之前哎媚,線程 2 調(diào)用了 lock(), 想想會發(fā)生什么?

線程 2 會初始化 head【new Node()】喊儡,同時線程 2 也會插入到阻塞隊列并掛起 (注意看這里是一個 for 循環(huán)拨与,而且設(shè)置 head 和 tail 的部分是不 return 的,只有入隊成功才會跳出循環(huán))

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

首先艾猜,是線程 2 初始化 head 節(jié)點买喧,此時 head==tail, waitStatus==0

aqs-1

然后線程 2 入隊:

aqs-2

同時我們也要看此時節(jié)點的 waitStatus捻悯,我們知道 head 節(jié)點是線程 2 初始化的,此時的 waitStatus 沒有設(shè)置淤毛, java 默認會設(shè)置為 0今缚,但是到 shouldParkAfterFailedAcquire 這個方法的時候,線程 2 會把前驅(qū)節(jié)點钱床,也就是 head 的waitStatus設(shè)置為 -1荚斯。

那線程 2 節(jié)點此時的 waitStatus 是多少呢埠居,由于沒有設(shè)置查牌,所以是 0;

如果線程 3 此時再進來滥壕,直接插到線程 2 的后面就可以了纸颜,此時線程 3 的 waitStatus 是 0,到 shouldParkAfterFailedAcquire 方法的時候把前驅(qū)節(jié)點線程 2 的 waitStatus 設(shè)置為 -1绎橘。

aqs-3

這里可以簡單說下 waitStatus 中 SIGNAL(-1) 狀態(tài)的意思胁孙,Doug Lea 注釋的是:代表后繼節(jié)點需要被喚醒。也就是說這個 waitStatus 其實代表的不是自己的狀態(tài)称鳞,而是后繼節(jié)點的狀態(tài)涮较,我們知道,每個 node 在入隊的時候冈止,都會把前驅(qū)節(jié)點的狀態(tài)改為 SIGNAL狂票,然后阻塞,等待被前驅(qū)喚醒熙暴。這里涉及的是兩個問題:有線程取消了排隊闺属、喚醒操作。其實本質(zhì)是一樣的周霉,讀者也可以順著 “waitStatus代表后繼節(jié)點的狀態(tài)” 這種思路去看一遍源碼掂器。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市俱箱,隨后出現(xiàn)的幾起案子国瓮,更是在濱河造成了極大的恐慌,老刑警劉巖狞谱,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乃摹,死亡現(xiàn)場離奇詭異,居然都是意外死亡芋簿,警方通過查閱死者的電腦和手機峡懈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來与斤,“玉大人肪康,你說我怎么就攤上這事荚恶。” “怎么了磷支?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵谒撼,是天一觀的道長。 經(jīng)常有香客問我雾狈,道長廓潜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任善榛,我火速辦了婚禮辩蛋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘移盆。我一直安慰自己悼院,他們只是感情好,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布咒循。 她就那樣靜靜地躺著据途,像睡著了一般。 火紅的嫁衣襯著肌膚如雪叙甸。 梳的紋絲不亂的頭發(fā)上颖医,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天,我揣著相機與錄音裆蒸,去河邊找鬼熄求。 笑死匆绣,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播岳枷,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼艾少,長吁一口氣:“原來是場噩夢啊……” “哼缩麸!你這毒婦竟也來了桂敛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤筷弦,失蹤者是張志新(化名)和其女友劉穎肋演,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烂琴,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡爹殊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了奸绷。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梗夸。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖号醉,靈堂內(nèi)的尸體忽然破棺而出反症,到底是詐尸還是另有隱情辛块,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布铅碍,位于F島的核電站润绵,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏胞谈。R本人自食惡果不足惜尘盼,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望烦绳。 院中可真熱鬧卿捎,春花似錦、人聲如沸爵嗅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽睹晒。三九已至,卻和暖如春括细,著一層夾襖步出監(jiān)牢的瞬間伪很,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工奋单, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留锉试,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓览濒,卻偏偏與公主長得像呆盖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子贷笛,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容