AQS(AbstractQueuedSynchronizer) 同步框架学习笔记
学习地址:https://www.bilibili.com/video/BV12K411G7Fg?t=544.9
思路
同步管理框架设计思路
目标: CAS只能原子的修改内存上的一个值,然而实际的业务场景中,需要同步的资源却是以对象的形式进行封装,如何利用CAS的特性对对象资源进行同步
- 通用性:下层实现透明的同步机制,与上层业务解耦
- 利用CAS的原子性,修改共享标志位:如果标志位为空,则表示当前资源空闲,如果标志位部位空,则表示当前资源正在被使用,线程需要等待
- 等待队列:阻碍其他线程的调用
- 使用场景:
- 线程1需要快速尝试一下获取共享资源,获取不到也没关系,会进行其他的逻辑处理。
- 线程2则需要必须获取到共享资源才能进行下一步的处理,如果当前时刻没有获取到,可以进行等待。
细节实现
AQS成员变量
/**
* Head of the wait queue, lazily initialized. Except for
* initialization, it is modified only via method setHead. Note:
* If head exists, its waitStatus is guaranteed not to be
* CANCELLED.
*/
private transient volatile Node head; // 队列的头节点
/**
* Tail of the wait queue, lazily initialized. Modified only via
* method enq to add new wait node.
*/
private transient volatile Node tail; // 队列的尾节点
/**
* The synchronization state.
* 同步状态表示资源是否被占用的标志位,volatile保证线程之间的可见性
* 在共享模式下,state还需要表示当前资源被多少个线程占用,所以使用int类型而不是boolean
*/
private volatile int state;
- 线程获取锁的两种模式:独占和共享
- 独占模式:一旦共享资源被线程占用,其他线程都必须等待(类似写锁)
- 共享模式:共享资源被线程以共享模式占用,则其他共享模式的线程也可以使用(类似读锁)
- 等待队列:如果一个线程在当前时刻没有获取到共享资源,可以选择进行排队。AQS的队列是一个FIFO(先进先出的双向链表)
队列中node的具体结构
static final class Node {
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
volatile int waitStatus; // 节点在队列中的等待状态,即上面的四种状态
volatile Node prev; // 前指针
volatile Node next; // 后指针
volatile Thread thread; // 线程对象
}
核心方法
- 利用state和FIFO等待队列来管理多线程的同步状态从而满足同步的使用场景:
- 线程1需要快速尝试一下获取共享资源,获取不到也没关系,会进行其他的逻辑处理。
- 线程2则需要必须获取到共享资源才能进行下一步的处理,如果当前时刻没有获取到,可以进行等待。
tryAcquire 方法
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
- 快速尝试一下获取共享资源,获取成功返回true,失败返回false
- int值参数代表对state的修改,布尔返回值代表是否成功获取锁,protected 上层可以override这个方法,写自己的逻辑
acquire 方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- 此方法定能够获取到锁
- public final 所有的继承类都直接调用这个方法,且不允许继承类擅自修改,这个方法一定能够获取到锁
- addWaiter方法,将当前线程封装成一个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;
}
// 如果当前线程 需要被挂起,并且成功挂起,interrupted 才为true
// parkAndCheckInterrupt是真正执行挂起的操作
// 挂起成功之后,当前线程会阻塞等待被唤醒
// 当当前节点成为头结点的next,则会被唤醒,则当前线程可以继续执行死循环,去尝试获取锁资源直到获取成功
if (parkAndCheckInterrupt(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 此方法只是判断线程是否有被挂起的资格
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 当前节点状态为SIGNAL,则返回true,上层会真正执行挂起当前线程的操作
if (ws == Node.SIGNAL)
return true;
// ws > 0只有一种情况,node节点状态为1(CANCELLED),代表线程被取消,所以直接跳过该线程
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 如果代码执行到这里说明,node状态不是-1 SIGNAL也不是1(CANCELLED),只能是剩下两种,将其状态置为SIGNAL,返回false
// 上层的死循环会接着执行此方法,此时则会走前面的判断,返回true
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// 真正挂起线程的方法
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);// park函数是将当前调用Thread阻塞,而unpark函数则是将指定线程Thread唤醒。
return Thread.interrupted(); // 设置线程的中断状态
}
- acquireQueued方法,配合release方法对线程进行挂起和响应,实现队列的先进先出
- 在AQS的FIFO队列中,头结点是虚节点,头结点是占有共享资源的节点,第二个结点才是需要去获取锁的结点,当第二个结点获取到锁之后,头结点会出队。
tryRelease 方法
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
- 快速尝试一下释放共享资源,释放成功返回true,失败返回false
release 方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
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;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 唤醒线程
if (s != null)
LockSupport.unpark(s.thread);
}
- 释放锁
- 如果成功释放锁,则将锁标志位置为0,然后则需要唤醒下一个队列中的线程,让他去获取锁,从后往前找到队列中最前面的一个状态不是已删除的线程,唤醒他让他去自旋获取锁,当前head节点出列。
AQS学习总结
线程一定能获取到锁闭环的形成过程
- 先尝试获取锁,能获取到则返回true,获取不到则将当前线程封装成node,加入等待队列插入队尾,并挂起当前线程
- 等待队列的头结点使用完资源,释放锁之后会从尾节点开始向前寻找最前面一个可用的node节点,并唤醒node封装的线程
- 线程一旦被唤醒,则会继续执行死循环来获取锁,成功获取到锁之后会经当前节点设置为头结点,之前的头结点出队
- 闭环形成
等待队列
- 所以一个等待队列中,只有头结点会占有锁,其他线程都处于被阻塞的状态,只有头结点即将释放锁时,第二个可用节点的线程会被唤醒然后执行死循环去获取锁资源