这里写目录标题
一、什么是AQS
aqs全称AbstractQueuedSynchronizer(队列同步器),它是用来构建锁或其他同步组件的基础框架。使用了一个int类型的成员变量来表示同步状态,通过FIFO(first input first out 先进先出)双向队列来完成资源获取线程的排队工作。
只有掌握了AQS的工作原理,才能更加深入的理解JUC的其他组件。
下图为AQS的同步队列基本结构:
二、AQS操作同步状态的方法
AQS使用了模板模式,推荐自定义的同步组件在内部使用静态内部类继承它来使用。AQS本身没有提供接口,只是提供了三个方法为自定义组件操作同步状态、同步队列:
- setState 设置同步状态的值。 此操作具有volatile写入的内存语义
- getState 返回同步状态的当前值。此操作具有volatile读取的内存语义
- compareAndSetState 使用cas将同步状态设置为给定的值。此操作具有volatile读写的内存语义
三、AQS需要实现的方法
独占模式
- protected boolean tryAcquire(int arg) 独占式获取同步状态
实现该方法需要先判断是否支持独占模式,再使用cas设置同步状态并返回true,如果获取失败返回false的话,则acquire可能将它放入同步队列进行排队并阻塞线程(如果它还没有排队),直到收到其它线程的释放信号。 - protected boolean tryRelease(int arg) 独占式释放同步状态
返回true,代表处于释放状态,等待获取同步状态的线程将有机会获取同步状态
共享模式
- protected int tryAcquireShared(int arg) 共享式获取同步状态
返回的值大于0表示获取成功,等于0表示获取成功但后续不能获取成功,小于0获取失败 - protected boolean tryReleaseShared(int arg) 共享式获取同步状态
- protected boolean isHeldExclusively() 同步器是独占的返回true,否则返回false
三、AQS等待状态详解
AQS类继承了各种场景的加锁框架,通过waitStatus来进行区分:
// 等待状态
volatile int waitStatus;
// 取消状态,该状态的节点代表已经被废弃,该出队被GC回收了
static final int CANCELLED = 1;
// 信号状态,该状态的节点代表它可以去唤醒后继节点,只有head才能拥有该状态
static final int SIGNAL = -1;
// 条件状态,该状态的节点为条件队列使用
static final int CONDITION = -2;
// 广播状态,共享式加锁使用
static final int PROPAGATE = -3;
三、AQS的主要方法流程
本文将从不同角度来演示AQS流程:
1、ReentrantLock独占式流程
2、阻塞队列-ArrayBlockingQueue
构造器
指定队列大小,使用公平锁还是非公平锁
// 指定队列容量大小,插入或删除访问队列时,无序处理
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
// 指定队列容量大小
// fair -> true 指定fifo访问队列(公平锁)
// fair -> false 无序访问队列(非公平锁)
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
// 初始化队列数据
this.items = new Object[capacity];
// 初始化锁
lock = new ReentrantLock(fair);
// 初始化条件队列,拿(take)元素时用到
// 如果上面
notEmpty = lock.newCondition();
// 初始化条件队列,存(put)元素时用到
notFull = lock.newCondition();
}
// c ——> 根据给定的集合初始化队列数据
public ArrayBlockingQueue(int capacity, boolean fair,
Collection<? extends E> c) {
this(capacity, fair);
final ReentrantLock lock = this.lock;
lock.lock(); // Lock only for visibility, not mutual exclusion
try {
int i = 0;
try {
for (E e : c) {
checkNotNull(e);
items[i++] = e;
}
} catch (ArrayIndexOutOfBoundsException ex) {
throw new IllegalArgumentException();
}
count = i;
putIndex = (i == capacity) ? 0 : i;
} finally {
lock.unlock();
}
}
初始化之後的数据结构图如下
put
public void put(E e) throws InterruptedException {
// 不能插入空值
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 加锁,如果是公平锁则根据FIFO规则判断CLH队列有线程在等待则进行入队自旋阻塞
// 如果是非公平锁则直接去拿一次锁,没拿到再入队自旋阻塞
lock.lockInterruptibly();
try {
// 执行到这里说已经拿到了锁
// 数组元素等于数组长度说明已经存满了,则
while (count == items.length)
notFull.await();
// 数组元素还没有满,执行入队
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
// 代码执行到这里断定持有锁的人只有一个
// 断定要插入的位置不存在元素
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
// 消费者条件队列发出信号,将消费者队列元素全部转移到CLH同步队列中参与拿锁
notEmpty.signal();
}
// 发出信号,将条件队列元素转移至CLH队列
public final void signal() {
// 持有锁的线程不是当前线程,直接抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 如果有开始等待者,则执行转移操作
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
// 将不是取消状态的节点转移至CLH队列,该方法只转移一个节点就返回
private void doSignal(Node first) {
do {
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// 将待转移节点不为空并且
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
// 原子性设置失败,则废弃该节点
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 入队
Node p = enq(node);
int ws = p.waitStatus;
// 判断前继节点是否被废弃了,如果被废弃则唤醒当前节点
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
transferForSignal方法最后的唤醒操作有些绕,这边需要拿出来重点解析,这里的代码需要结合前面的代码来分析:
- 代码能执行到这个方法,必然满足signal方法的first != null,也就是说put方法的count == items.length为true了,有线程已经在await方法里面阻塞了,假设items.length等于10,那么执行transferForSignal方法的线程为11
- transferForSignal是从条件队列头节点开始转移,并且只转移一个,也就是说当前进入transferForSignal方法的线程是11,而参数node是线程10,它已经是阻塞状态
- 那么判断线程10在CLH队列中的前继节点是否废弃掉了,如果废弃掉了,就将线程10唤醒,继续执行await方法-》acquireQueued()-》shouldParkAfterFailedAcquire() 进入状态检测,如果前继节点已经取消,则移除前继节点,如果是SIGNAL则阻塞自己等待前继节点的唤醒。
- 上面的第三步唤醒操作是必要的,从高并发场景来看,如果没有上述唤醒操作,转移到CLS队尾的那个节点就不会执行shouldParkAfterFailedAcquire方法去检测被废弃的前继节点,而被废弃的前继节点在高并发场景可能存在很多个,这些废弃的前继节点只有等到头节点释放锁的时候找到离头节点最近的那个节点唤醒它,被唤醒的节点将继续执行await方法-》acquireQueued()-》shouldParkAfterFailedAcquire() 进行检测。由于它是离头节点最近的,导致后面很多废弃节点都没有被废弃掉,也将影响释放锁时的性能,因为释放锁时可能需要遍历节点寻找离头节点最近且未被废弃的节点。
- 因此,第三步的唤醒操作从性能角度来看,是必要的,它将提前将废弃节点的废弃掉,并且被GC回收