Java AQS锁源码深入原理讲解

从源码角度带你理解AQS

作者编写本文的方式,同时建议给您一种阅读方式:

  • 本文所有的代码都是源码,可在JDK8中找到,讲解的代码无删减都在本文;
  • 本文的所有代码块及其中的属性和方法,均采用从上到下顺序讲解;
  • 本文代码中文注释才是重点,注释顺序也是按照从上到下阅读;
  • 如有错误和建议、提问等,都可以与我联系,互相学习哦!
1. 下面这三个字段是AQS这个类自己的属性定义,我们先理解一下,AQS想要定义一个什么样的东西:
    /**
     * 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;

    /**
     * The synchronization state.
     */
    private volatile int state;
2. 首先,先大致理解下这些属性的意义:
  • head:一个头节点;
  • tail:一个尾节点。咦?!这不是LinkedList的双向链表结构嘛,其实人家真正的名字叫CLH同步队列;
  • state:那这个就是链表全局的同步状态的标识喽,先透露一下,这个值可以被用来做重入锁的次数。
    private static final long stateOffset; // CAS字段偏移量,对应的是 AQS中 state属性操作
    // 全局唯一方法,外部类只能通过这个方法来操纵 state属性
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

请看如下代码步骤和详细注释,一个完整的加锁入口 acquire()方法(形如:Lock.lock()):

    // 获取锁的开始方法,参数是一个状态次数(你也可以叫重入次数)
    public final void acquire(int arg) {
        if (!tryAcquire(arg) && // 先尝试获取一次锁,获取不到就是False,获取到那直接就获取了锁,就不用操作其他的了
            // 当尝试一次后获取不到,addWaiter方法就开始加入CLH队列了(Node.EXCLUSIVE为独占锁模式,可先忽略)
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // acquireQueued方法在下个小节中讲解
            selfInterrupt();
    }

    /**
     * 把一个新节点(抢占线程)插入链表尾部
     *(整体操作和双向链表一直,唯一区别是链表节点中的前驱和后驱节点的修改需要加 CAS锁)
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 先尝试一次将新节点快速插入链表尾部,如果CAS失败,则进入接下来的enq方法。
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 循环将新节点插入链表尾部,直到插入成功(其实与上面先尝试一次大致相同,唯一区别是加入了链表为空的处理,一切为了性能,哈哈哈)
        enq(node);
        return node;
    }

把上面代码的注释看完后,我们先暂停一下,此时我们已经或多或少的知道AQS的整体是一个同步队列了,也大致可以想象一个CLH同步队列的样子了,也知道怎样将一个线程(节点)放入CLH队列中了,唯一的模糊在于同步状态 state有什么用,在哪里用?

如果只局限于AQS这个抽象类来说,其并没有定义重入的概念,但是支持重入锁的扩展设计的。
它只抽象于CLH的同步队列的处理,而重入的概念是交由像ReentrantLock、Sync等高层次类去实现的。
从抽象层面来说,它提供了status字段来让你自定义标识,同时还提供了像tryAcquire、tryRelease的扩展方法来让你实现自己的效果。

【自行脑补插入一个CLH同步队列图,其实就是一个双向链表】

3. 接下来我们先全局的看下,加入队列的节点(线程)接下来要做哪些事情:
    /**
     * 这个方法的目的,就是教你怎样去排队等待。
     * 例:
     * 就像排队买早点一样,只有一个窗口。某一天,你来的早不如来的巧,正好前面一小美女刚买完早点,你就直接问老板要了个豆腐脑,老板直接给你了(头节点一次尝试)。
     * 第二天,我的妈,这么老长的队,你前面有人上班等不及了提前跑了(线程中止前移),那你就能往前走走。
     * 这个早点铺呢,还有个奇葩又人人遵守的规矩,买完饭的人要向后面那个人打个招呼?!(线程等待释放通知)
     *
     * @param node the node
     * @param arg the acquire argument
     * @return {@code true} if interrupted while waiting
     */
    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)) { 
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                // 设置一个合适的链表节点位置(也可以称为合适时机)让其线程等待,(在此期间,可能会有前驱节点中断或者已运行完成等情况,所以要设置一个合理位置)
                if (shouldParkAfterFailedAcquire(p, node) &&
                    // 如果找到合适时机(true),则触发逻辑表达式 parkAndCheckInterrupt方法调用让线程等待
                    // 此时,线程在此进入wait的状态,等待其他线程唤醒或者被中止,这是重点,后续让你了解如何解锁的
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node); // 执行失败移除,或通知后续节点继续,第 5 小节会细说
        }
    }

    /**
     * 设置一个合适的链表节点位置(也可以称为合适时机)让其线程等待,(在此期间,可能会有前驱节点中断或者已运行完成等情况,所以要设置一个合理位置)
     *
     * @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;
        if (ws == Node.SIGNAL)
            /*
             * 前驱节点的 waitStatus = SIGNAL状态表示:后继节点需要被通知释放(后驱节点这时候在一直阻塞等待)
             */
            return true;
        if (ws > 0) { // waitStatus 状态大于0,代表的是线程已经取消或终止了
            /*
             * 我们找到前驱节点没有被取消或终止的那一个。就像早点铺来不及排队的人走掉一样,你要接上这个队伍,然后重试获取一个等待时机
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 大致意思就是,告知前驱节点,你执行完后,我需要被通知释放,然后重新获取一次等待时机
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

    /**
     * 执行线程阻塞等待,并在唤醒后检查线程是否中止
     *
     * @return {@code true} if interrupted
     */
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

看到这里,我们已了解到加入CLH队列的节点(线程)会自己做一些排队的处理,我们也该再整理一下Node节点的属性和作用了,这样我们再反过来回顾一下,会更加清晰!

4. Node节点的属性和意义:
  • waitStatus:我们在上文中看到了大量的该状态,主要代表节点的等待状态表示方式,具体看注释;
  • prev:关联前驱节点;
  • next:关联后驱节点;
  • thread:节点绑定的工作线程;
  • nextWaiter:锁的模式,我们在起初的获取锁的入口方法里,有一个Node.EXCLUSIVE,代表是独占模式。
        volatile int waitStatus;
        volatile Node prev;
        volatile Node next;
        volatile Thread thread;
        Node nextWaiter;

        // 下面这些字段,代表着 waitStatus的一些状态值
        /** 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 ,使用Condition方式形成的等待,可先忽略,后续在讲*/
        static final int CONDITION = -2;
        /**
         * waitStatus value to indicate the next acquireShared should unconditionally propagate ,共享锁下的无条件传播模式,后续在讲
         */
        static final int PROPAGATE = -3;
5. 节点(线程)中断或者失败的释放

在第 3 小节中,我们已经成功的线程持有锁或者加入CLH的等待队列中,但是还没完。
我们知道,线程会有被终止的风险,或者加锁失败,那后续这个节点还在队列中,那怎么办呢?
请看下面详细源码及注释:

    /**
     * 取消获取锁操作
     *
     * @param node the node
     */
    private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null; // 先将持有线程设置为 null

        // 跳过所有被终止或失败的前驱节点,去找正常的前驱节点
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // 获取前驱节点的后驱节点,理论上此时在高并发情况下,该next可能已经不是node
        Node predNext = pred.next;

        // 这个设置不需要CAS,因为在别的节点检查时,会跳过该节点
        node.waitStatus = Node.CANCELLED;

        // 如果当前节点为CLH尾节点,并且可以原子性的修改尾节点,则直接完成取消操作(此时,没有人排在你后面)
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // 如果有人还在你后面排队,那你就要通知他释放或者前移
            // 当你前面排队的人很多(当前节点不是头节点后面那个),则尝试告知前面那个人说:你一会去通知我后面那个人,我等不及买早点了撤了(前提是,你后面有人且他打算继续排队)。
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                // 如果上面情况都没一次性尝试成功,那么就按个的进行尝试通知,让队伍接上(唤醒后驱节点)
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

    /**
     * 唤醒后驱节点
     *
     * @param node the node
     */
    private void unparkSuccessor(Node node) {
        /*
         * 这种情况时正常解锁判断的,我们后续在解锁中会讲,目前已取消是必然的 waitStatus = CANCELLED > 0
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * 我们开始找后驱节点,如果后驱节点不存在或者后驱节点也处于终止状态,
         * 则干脆直接从CLH尾部开始向前找合适的后驱节点,找到之后,通知其释放等待
         */
        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); // 通知后驱释放等待,开始获取锁
    }

至此,一个简单且完整的获取锁操作便完成了他的工作。

注:当你深度思考后,你会发现,当AQS在某种情况下,会有概率出现静默问题,但这应该是极小的情况,可以忽略。静默问题指中间链表中断,此时前驱无任务,后驱需要重新从tail找到可用节点所需时间内静默的状态,我称其为静默状态。

6. 利用AQS如何完成解锁的操作

解锁就简单很多了,比较在加锁中也有解锁的身影了?哪里哦,你个赖皮??答案就是节点终止或失败。

请看如下代码步骤和详细注释,一个完整的解锁入口 release()方法(形如:Lock.unlock()):

    /**
     * 释放节点(线程),tryRelease()是抽象方法,交由上层具体类实现,其意思是尝试一次解锁
     *
     * @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) // 当头节点不为空且包含有信号通知时,则出发释放通知(向后驱节点发送)
                unparkSuccessor(h); // 这个就是第五小节基本一致了(终止操作)
            return true;
        }
        return false;
    }

到此,整个一套加锁、解锁的流程就走通了,但上面简述的只是独占锁的方式。

7. 共享锁的了解

本章节节中,如果与独占锁相同的逻辑地方不再讲述,只会一笔带过。对于共享锁,原理大致和独占相似,
最大的不同就是共享锁中 setHeadAndPropagate方法,设置头节点,然后通知后驱共享节点释放。

    // 共享方式获取,入口
    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0) // 尝试一次获取锁,继承AQS实现
            doAcquireShared(arg); // 循环尝试获取共享锁
    }

    /**
     * 共享模式下,和独占锁模式先逻辑大致相同:把一个新节点(抢占线程)插入链表尾部
     * @param arg the acquire argument
     */
    private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED); // 在独占锁中 addWaiter的入参是Node.EXCLUSIVE
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg); // 独占锁模式返回的是boolean值,共享模式返回int值,负值为获取失败
                    if (r >= 0) {
                        setHeadAndPropagate(node, r); // 最大的区别在这里,我们在下面看下这个方法
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    /**
     * Sets head of queue, and checks if successor may be waiting
     * in shared mode, if so propagating if either propagate > 0 or
     * PROPAGATE status was set.
     * 
     * 由于是共享模式,极端情况下可能会同时存在于两个节点竞争head,前提是在一次尝试后为获取到锁,
     * 理论上,共享模式下在最早的第一次 tryAcquireShared调用下就可以成功,除非资源不够才会调用 addWait放入队列。
     *
     * @param node the node
     * @param propagate the return value from a tryAcquireShared
     */
    private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below 第一次设置前没有head节点
        setHead(node); // 在独占模式中,仅仅只用调用这一个方法
        
        // 如果尝试获取成功,则设置该节点为头节点,
        // 保证 propagate资源足够、头节点为null(没有正在运行的节点)、需要通知后驱节点状态的其中一个条件的情况下,
        // 且后驱节点是null或是共享模式,则依次传播释放其他正在等待的共享节点
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared(); // 共享传播,释放其他共享节点
        }
    }

    // 共享模式下的释放通知,包括解锁,也采用该方法
    private void doReleaseShared() {
        // 依次唤醒下一个节点,当排队节点阻塞等待时,一定会设置好前驱节点的通知状态
        for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;            // loop to recheck cases
                    unparkSuccessor(h);
                }
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            if (h == head)                   // loop if head changed
                break;
        }
    }

注:doReleaseShared()方法需要结合上下文看,这玩意挺绕人,哈哈哈!

求助:

我猜测一个问题,假设没有acquireShared方法中那第一次尝试获取,当 头节点和当前节点同时进入setHeadAndPropagate方法时,当前节点会替换掉头节点,原来的头节点已经消失。此时,无法修改原来头节点的 waitStatus通知状态,且当前节点既是head又是tail,导致在通知时直接跳过,也不做任何释放,难道CLH队列一直保留着最后一个废弃的节点???
下面这个操作一方面是设置头节点,另一方面是不是移除必要信息,形成一个空头节点(只有next属性)呢?
如有大神请帮忙展开讲讲,谢谢!

    /**
     * Sets head of queue to be node, thus dequeuing. Called only by
     * acquire methods.  Also nulls out unused fields for sake of GC
     * and to suppress unnecessary signals and traversals.
     *
     * @param node the node
     */
    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容