以下是个人阅读源码时的一些疑问及个人解答,不涉及源码论证,如果想结合源码来论证的话,可以先去看:JUC——AQS源码解析
英文好的也可以去看看https://gee.cs.oswego.edu/dl/papers/aqs.pdf,这份pdf里写了很多AQS设计的初衷以及为什么这么设计
1.Node节点的next指针是可靠的嘛?
不可靠,这与节点入队方法enq有关。
节点入队时,步骤依次为:1.节点prev指向尾节点——2.CAS更新节点为尾节点——3.前任节点(旧尾节点)next指向该节点(现任尾节点)。
从步骤可看出,在CAS更新了尾节点后,若此时发生了线程调度,那么其他线程使用队列时,可能就会发现某个节点的next为null,但实际上只是还未来得及更新而已,这时就需要使用尾部遍历来检查队列。
(引申:队列是可能出现暂时性分叉的,假设在并发的极端情况下,多个线程都执行了上述步骤1,此时多个节点指向prev,但只有一个节点能成功执行步骤2,其他线程会继续循环,直至入队成功。这种情况是暂时的,而且并不会影响到队列)
2.Node节点的prev指针是可靠的嘛?
可靠。
从问题1可知,节点是会先更新节点的prev指针,然后再CAS更新尾节点。虽然这种情况可能会在极端情况下出现短暂的队列分叉,但是也确保了成功执行CAS的节点的prev指针不可能为空,它的prev必然正确指向前任节点。
3.AQS中为什么几乎使用尾节点遍历而不使用头节点遍历?(例如:节点唤醒方法unparkSuccessor等)
因为next指针不可靠,而prev节点可靠。从尾节点依靠prev遍历,必然能遍历到整个队列(当然,还未正式入队列的节点不算)
4.AQS为什么使用双向链表?
先说结论,AQS中希望队列在入队时只需要对tail操作,在出队时只涉及head更新。部分是为了处理取消和中断操作。这是AQS中Node类的注释原话,该注释还分别介绍了prev和next的作用:
prev指针主要是处理取消(也就是中断锁的获取),当节点取消时,它的后继节点会重新链接到未取消的前任节点
next指针实现阻塞机制(也就是阻塞唤醒机制),当线程释放锁时,会唤醒后继节点
个人看法,使用双向链表,就是为了更快:
- 入队时,直接更改当前节点的prev和前任节点的next,即可完成入队
- 抢锁时,直接获取当前节点的prev,就能知道是否应该抢锁(prev是否指针头节点),若不满足抢锁条件,也可以设置prev节点的状态后将自己休眠,避免持续占用CPU
- 取消锁时,直接获取当前节点的prev及next,将前任节点的next指针及后继节点的prev指针更改下,即可将当前节点移出队列
- 释放锁时,直接获取当前节点的next,将该节点唤醒即可
- 出队时,被唤醒的节点在抢锁完成后,直接将自己的prev改为null,前任节点就被移出了队列
上面列举了大部分使用到prev及next的地方(可能会漏掉),我们可以看到,如果不使用双向链表,这里面很多场景都要去从头遍历,很浪费效率,而使用了双向链表,几乎能非常快速的完成操作
当然了,上面也只是理想情况下的操作,如果出现了极端情况,也是会从尾节点遍历进行节点检查(例如:某节点next为null或者为已取消节点,那么此时无法唤醒next指向的节点,只能通过tail尾节点来向上遍历,找到最近的一个正常节点。)