什么是 AQS
AbstractQueuedSynchronizer,一个抽象类,用来构建锁和同步器,定义了资源获取和释放的通用流程,ReentrantLock、Semaphore 皆是基于 AQS 实现的。
核心思想
线程请求的共享资源已被占用,那么该请求线程进入 AQS 的 CLH 队列进行等待,否则把请求线程设置为有效的工作线程,并将共享资源设置为占用状态,即该线程占用了共享资源。
AQS 的 CLH 锁队列
自旋锁即线程不断对一个原子变量 CAS 来尝试获取锁,若多线程同时竞争同一个原子变量,可能造成某个线程的
CAS操作长时间失败,从而导致 “饥饿"问题 ,而 CLH 锁对自旋锁进行了改进,通过引入一个单向队列来让线程排队确保公平性,避免饥饿。AQS 在 CLH 锁的基础上进一步优化,形成了其内部的 CLH 队列变体,优化点如下:
- 自旋+阻塞:普通的 CLH 锁使用纯自旋等待锁释放,大量自旋会占用 CPU 资源,AQS 的 CLH 锁则会短暂自旋,失败后进入阻塞状态,等待被唤醒,减少 CPU 占用。
- 双向队列 :普通的 CLH 锁是单向的,节点只知道前驱节点的状态,而当某个节点释放锁时,需要通过队列唤醒后续节点。AQS 将队列改为 双向队列 ,新增了
next指针,使得节点不仅知道前驱节点,也可以直接唤醒后继节点,从而简化了队列操作,提高了唤醒效率。
|
|


Node 节点各个状态含义

为什么 state 要用volatile 修饰
使用 volatile 修饰 state 不是为了利用 volatile 的内存可见性,因为 state 本来就只会被持有线程写入,只会被队列中该线程的后驱节点对应的线程读,而且后者会轮询读取。因此,可见性问题不会影响锁的正确性。
但要实现一个可以在多线程程序中正确执行的锁,还需要解决重排序问题 。
在《Java 并发编程实战》一书对于重排序问题是这么描述的:在没有同步的情况下,编译器、处理器以及运行时等都可能对操作的执行顺序进行一些意想不到的调整。在缺乏足够同步的多线程程序中,要想对内存操作的执行顺序进行判断,几乎无法得到正确的结论。对于 Java synchronized 关键字提供的内置锁(又叫监视器) ,Java 内存模型规范中有一条 Happens-Before(先行发生)规则:“一个监视器锁上的解锁应该发生在该监视器锁的后续锁定之前"也就是后面的锁在锁定之前得知道前面的锁有没有解锁,而自定义互斥锁就需要自己保证这一规则的成立,因此上述代码通过 volatile 的 Happens-Before(先行发生)规则来解决重排序问题。JMM 的 Happens-Before(先行发生)规则有一条针对 volatile 关键字的规则:“volatile 变量的写操作发生在该变量的后续读之前”。
AQS 的独占和共享
Exclusive(独占,如ReentrantLock)和Share(共享,如Semaphore/CountDownLatch)。一般来说,自定义同步器的共享方式要么是独占,要么是共享,他们也只需实现
tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
acquire()和 release()
acquire()
|
|
release()
|
|
以 ReentrantLock 讲解 AQS 原理
假设有 3 个线程尝试抢占 获取锁,线程分别为
T1、T2和T3。假设线程
T1先获取到锁,线程T2排队等待获取锁。但是在线程T2进入队列之前,需要初始化 AQS 的 CLH 锁队列。head节点在初始化后状态为0。AQS 内部初始化后的队列如下

由于线程
T1持有锁,因此线程T2会获取失败并进入队列中等待获取锁。同时会将前继节点(head节点)的状态由0更新为SIGNAL,表示需要对head节点的后继节点进行唤醒。此时,AQS 内部队列如下图所示:

由于线程
T1持有锁,因此线程T3也获取锁失败,会进入队列中等待获取锁。同时会将前继节点(线程T2节点)的状态由0更新为SIGNAL,表示线程T2节点需要对后继节点进行唤醒。此时,AQS 内部队列如下图所示:

此时,假设线程
T1释放锁,会唤醒后继节点T2。线程T2被唤醒后获取到锁,并且会从等待队列中退出(不是移除,因为 T2 还要当 head)。
