耐心读懂AQS节点入队操作

1 缘起

为啥AQS先从入队讲起?
因为,我在项目中,使用了CountDownLatch,也是抄过来的,不懂原理。
于是,复盘时,看了CountDownLatch源码,
发现CountDownLatch继承了AbstractQueueSynchronizer,
执行await方法时,使用了addWaiter方法,这里有入队的方法enq(…),
特摘写这部分入队操作,佩服作者的设计。
先写为敬。

2 入队

2.1 源码

入队源码如下,
位置:java.util.concurrent.locks.AbstractQueuedSynchronizer#enq
这段代码,很精简,一个字:好。
每行代码都添加了注释,为了全面理解,我把每一步都配了一张图,见后文,耐心读。
AQS构造的队列为双向链表,并且使tail指向最新入队的节点,即每次新入队的节点都会用tail标识,
保证,下一次有新节点入队时,可是通过tail直接找到,无需遍历,提高插入效率。
入队出队模型:
在这里插入图片描述

注意:

每行最后的注释都有标号,下文,按照标号进行逐行讲解。


    /**
     * Inserts node into queue, initializing if necessary. See picture above.
     * @param node the node to insert
     * @return node's predecessor
     */
    private Node enq(final Node node) {
    	// 自旋锁
        for (;;) {
        	// 指向tail,(t->tail)即插入节点之前队列的末尾,中间变量,用于插入节点
            Node t = tail;// (1)
            if (t == null) { // Must initialize
            	// 设置Node头head->new Node()
                if (compareAndSetHead(new Node()))// (2)
                	// tail指向head,即指向new Node(),tail->new Node()
                    tail = head;// (3)
            } else {
            	// 插入的节点prev指向tail,node->prve->t
                node.prev = t;// (4)
                // tail指向插入的节点,tail->node
                if (compareAndSetTail(t, node)) {// (5)
                	// 原tail节点next指向新节点,t->next->node
                    t.next = node;// (6)
                    return t;
                }
            }
        }
    }

2.2 第一个节点入队Node-1

AbstractQueuedSynchronizer是抽象类,因此只能继承,
通过继承的类初始化,初始head=null,tail=null如下图所示。
在这里插入图片描述

(1)Node t = tail:t->tail

在这里插入图片描述

(2)compareAndSetHead(new Node()):head->new Node()

初始化节点,并将head指向它,如下图所示。
在这里插入图片描述
通过方法compareAndSetHead(new Node())新建节点并使head指向它。
源码如下图所示,通过unsafe的方法:compareAndSwapObject,判断head是否为null,为null,则将head更新为update,;不为null,一直循环(自旋),直到更新结束。
在这里插入图片描述
其中,headOffset是通过反射获取head属性,再通过unsafe.objectFieldOffset获取,源码如下图。
在这里插入图片描述

(3)tail = head

head初始化后,需要为tail指定一个节点,因为初始化,只有一个节点,
所以,将tail指向new Node(),与head指向同一个节点,结果如下图所示。

在这里插入图片描述

第二次循环

下面进入第二次循环,由于初始化完成,tail不再为null,
因此,进入else逻辑。

(4)node.prev = t

其中,node为待入队的节点,
这一步为待入队的节点(node)添加前节点,即node->prev->new Node(),结果如下图所示。
在这里插入图片描述

(5)compareAndSetTail(t, node)

这一步是将tail指向新节点,保证通过tail可以直接找到最新添加的节点,结果如下图所示。

在这里插入图片描述
compareAndSetTail即CAS,将tail更新为新加入的节点,即tail指向新入队节点,tail->node。
tail中的值为expect时,更新tail为update。
传入的expect为t,即tail,
update为node,将tail更新为node。
在这里插入图片描述
其中,通过反射获取tailOffset,源码如下图所示。
在这里插入图片描述

(6)t.next = node

为待入队的节点(node)分配前节点(prev)之后,
接下来需要将待入队节点的上一个节点next指向待入队节点,以形成双向,
即new Node()->next->node,结果如下图所示。
在这里插入图片描述

结果

第一个节点入队结果如下图所示,此时,tail指向最新入队的节点Node-1。
在这里插入图片描述

2.3 第二个节点入队Node-2

(1)Node t = tail:t->tail

在这里插入图片描述

(4)node.prev = t

在这里插入图片描述

(5)compareAndSetTail(t, node)

在这里插入图片描述

(6)t.next = node

在这里插入图片描述

结果

第二个节点入队结果如下图所示,此时,tail指向最新入队的节点Node-2。
在这里插入图片描述

3 小结

(1)AQS构造的队列为双向链表,先进先出原则;只能保证先入队先获取机会,不能保证一定成功,若失败, 则继续等待;
(2)入队:新节点添加到队尾,使tail指向最新入队的节点,即每次新入队的节点都会用tail标识,
保证,下一次有新节点入队时,可是通过tail直接找到,无需遍历,提高插入效率;
(3)出队:设置head即可;
(4)使用compareAndSetHead和compareAndSetTail保证自旋。

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天然玩家

坚持才能做到极致

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值