参考链接:https://www.bilibili.com/video/BV12K411G7Fg
通过 CAS ,我们可以实现乐观锁操作,从而使得线程进行同步,但是通过 CAS 的源码,我们发现 CAS 仅仅能修改内存中的一个值,而不是对对象进行同步,那么该如何对对象进行同步呢?同时,在多线程对统一资源进行竞争的情况下,如何能管理到所有需要该资源的线程呢?于是,AQS应运而生。
参考:《深入 Java 虚拟机》
AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,并发包中锁的底层就是通过AQS实现。结构如下
属性
int state
在共享模式下,需要表示共享锁的持有线程数量。
共享锁 和 独占锁(排他锁)
共享锁:该锁允许被多个线程持有,共享锁仅支持读数据,如果一个线程对数据加了共享锁后,其他数据只能对该数据加共享锁。
独占锁(排他锁):只有一个线程能获得锁。
共享锁 和 独占锁是 AQS 的不同实现方式
Node head & Node tail
用于维护一个 FIFO 的双向链表,两个 Node 节点分别指向头节点和尾节点
Node
队列中的节点,结构见上图
方法(以独占模式为例)
tryAcquire(int arg)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
尝试获取锁,获取锁失败直接返回。
该方法仅仅抛出一个异常,AQS 继承类需要继承该方法,用于给上层开放空间,使用户能编写业务逻辑。
acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
tryAcquire
方法失败后,会进入等待队列
addWaiter(Node.EXCLUSIVE), arg)
主要作用为新建一个 Node 节点,并将节点插入等待队列。
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 获取当前尾节点,tail 是 AQS 的属性
Node pred = tail;
if (pred != null) {
node.prev = pred;
// 尝试通过原子操作将当前节点置为尾节点
// 其实获取 pred 后,其他线程也可能会对 tail 进行修改
// compareAndSetTail(Node expect, Node update) 会读取 tail 的偏移
// 判断当前的 pred 是不是还是队尾(期间可能被其他线程修改),若是,则更新队尾为当前 node
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 调用完整的入队方法,上面尝试快速入队失败时会进入该方法
// 例如 tail 被修改的情况
enq(node);
return node;
}
acquireQueued(final Node node, int arg)
加入队列后,在队列中自旋对锁进行获取。
经过代码可以看出,head 节点后的节点组成了等待队列
当 head 后第一个 node 获得锁时,node 会成为新的头节点
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) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
boolean tryRelease(int arg)
同 tryAcquire
,作为开放给上层的方法
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
boolean release(int arg)
释放锁,并通知队列,改变等待队列中的线程状态
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 传入 head 并唤醒等待队列中的 node
unparkSuccessor(h);
return true;
}
return false;
}
unparkSuccessor(Node node)
头节点操作完资源后,通知等待队列中的节点
下方代码的操作中,为什么唤醒不从头节点开始呢
该处搜索并不是原子性的,从后往前搜索,可能会因为队列构建顺序未
- 后节点 pre 指向前节点
- 前节点 next 才会指向后节点
从前往后可能会因第2步还未完成而造成搜索中断
private void unparkSuccessor(Node node) {
// 设置头节点的状态
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 从尾节点开始搜索,head 后最靠前的节点并且 waitStatus <= 0 的节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 对找到的节点进行唤醒操作,唤醒后会自旋执行 acquire 方法获取锁
if (s != null)
LockSupport.unpark(s.thread);
}
共享模式
共享模式下,锁可以被多个线程获取,表现为 state 值的增加。
线程使用锁操作完成后,对锁进行释放,同时 state 减少。
锁的获取:
使用锁资源的锁释放后
- 独占模式:仅会唤醒最靠前的节点
- 共享模式:唤醒队列所有处于共享模式下挂起状态的节点