参考答案
1. AQS是什么
- 前往查看:AQS是什么
2. AQS 的内部实现
- AQS 队列内部维护的是一个 FIFO 的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的后继节点和直接前驱节点,所以双向链表可以从任意一个节点开始很方便的访问前驱和后继。
- 每个 Node 其实是由线程封装,当线程争抢锁失败后,会封装成 Node 加入到 ASQ 队列中。当获取锁的线程释放锁后,会从队列中唤醒一个阻塞的节点(线程)。
Node的组成:
static final class Node { static final Node SHARED = new Node(); static final Node EXCLUSIVE = null; static final int CANCELLED = 1; static final int SIGNAL = -1; static final int CONDITION = -2; static final int PROPAGATE = -3; volatile int waitStatus; volatile Node prev;//前驱节点 volatile Node next;//后驱节点 volatile Thread thread;//当前线程 Node nextWaiter;//存储在condition 队列中的后继节点 final boolean isShared() {//是否为共享锁 return nextWaiter == SHARED; } final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { // Used to establish initial head or SHARED marker } Node(Thread thread, Node mode) { // Used by addWaiter 将线程构造一个节点,添加队列 this.nextWaiter = mode; this.thread = thread; } Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; } }
3. 释放锁以及添加线程对于队列的变化
当出现锁竞争、以及释放锁时,AQS 同步队列中的节点会发生变化。
下面我们来看添加节点的场景:
这里涉及两个变化:
- 新的线程封装成 Node 节点,追加到同步队列中,设置 prev 节点、以及修改当前节点的前置节点的 next 节点指向自己。
- 通过CAS讲tail重新指向新的尾部节点。head 节点表示获取锁成功的节点,当头结点在释放同步状态时,会唤醒后继节点,如果后继节点获得锁成功,会把自己设置为头结点。
节点的变化过程如下图:
这个过程中,同样涉及到两个变化:
- 修改 head 节点指向下一个获得锁的节点。
- 新的获得锁的节点,将 prev 的指针指向 null。
设置 head 节点不需要用 CAS,因为设置 head 节点是由获得锁的线程来完成的,而同步锁只能由一个线程获得,所以不需要 CAS 保证,只需将 head 节点设置为原首节点的后继节点,并且断开原 head 节点的 next 引用。
4. 源码分析
以 ReentrantLock 作为切入点,我们来看看,在这个场景中是如何使用 AQS 来实现线程的同步的。
4.1 ReentrantLock 的时序图
调用 ReentrantLock 中的 lock()方法,源码的调用过程我使用了时序图来展现。
4.2 ReentrantLock.lock()
reentrantLock 获取锁的入口:
public void lock() { sync.lock(); }
sync 实际上是一个抽象的静态内部类,它继承了 AQS 来实现重入锁的逻辑。
我们前面说过 AQS 是一个同步队列,它能够实现线程的阻塞以及唤醒,但它并不具备业务功能,所以在不同的同步场景中,会继承 AQS 来实现对应场景的功能。
Sync 有两个具体的实现类,分别是:
- NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他线程等待,新线程都有机会抢占锁。
- FailSync: 表示所有线程严格按照 FIFO 来获取锁。
4.3 NofairSync.lock
以非公平锁为例,来看看 lock 中的实现。
- 非公平锁和公平锁最大的区别在于,在非公平锁中,我抢占锁的逻辑是,不管有没有线程排队,我先上来 cas去抢占。
- CAS 成功,就表示成功获得了锁。
- CAS 失败,调用 acquire(1)走锁竞争逻辑。
final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
以上,是Java面试题【AQS的底层原理是什么】的参考答案。
输出,是最好的学习方法。
欢迎在评论区留下你的问题、笔记或知识点补充~
—end—