AQS
AQS(AbstractQueuedSynchronizer)是Java并发编程中的一个底层同步工具类,它是一个用来构建锁和同步器的框架。
AQS维护了一个共享资源状态(用volatile int State表示),并通过CAS操作完成对State值的修改。它使用一个先进先出(FIFO)的线程等待队列来完成资源获取的排队工作。当有自定义同步器接入时,只需重写第一层所需要的部分方法即可,不需要关注底层具体的实现流程。同时,Java中的大部分同步类(Lock、Semaphore、ReentrantLock等)都是基于AbstractQueuedSynchronizer(简称为AQS)实现的。
AQS 总体结构图
AQS 中 维护了一个 volatile 修饰的 state 变量(代表共享资源)和一个 FIFO 线程等待队列(多线程争用资源被阻塞时会进入此队列)通过 CAS来操作 state 保证在多线程情况下的安全性
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.
*/
private volatile int state;
AQS 有两种队列,一种是阻塞队列,另一种是等待队列,等待队列都是通过双向链表来实现的,阻塞队列是通过单向链表来实现的,其中等待队列只有一个,阻塞队列可以有多个
需要注意的是:二者复用了同一种数据结构 Node
等待队列:非公平锁情况下线程首先尝试获取资源,失败后会进入等待队列队尾,给前继节点设置一个唤醒信号后,自身进入等待状态,直到被前继节点唤醒;公平锁的情况下,线程不会尝试获取资源,而是直接进入等待队列队尾
条件队列:是为 Condition 实现的一个同步器,一个线程可能会有多个条件队列,只有在使用了Condition才会存在条件队列。需要注意的是,如果一个线程被唤醒后,它会从条件队列转移到同步队列来等待获取锁,后面对条件队列进行源码分析时会再详细讲解。
在看源码之前,可以做一些猜测
- 为什么获取 Condition 时需要先获取锁?
需要将阻塞队列绑定到特定的锁当中?还是别的原因?
- 独占锁的实现方式是怎样的?
每次仅允许等待队列的队头去访问 state ,因此可以保证每次仅有一个线程操作 state 变量。那么是如何保证仅允许队头元素去访问 state 呢?是通过 CAS 吗?
ReentrantLock
由于 AQS 对锁的操作没有实现,因此我们通过分析 ReentrantLock 中的加锁代码
// ReentrantLock.java 文件
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
if (compareAndSetState(0, 1)) // 因为是非公平锁,所以在一开始的时候会先尝试获取锁,获取失败再与等待队列的线程进行争抢
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
static final class FairSync extends Sync { // 公平锁
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && // 判断是否有等待队列,如果有等待队列且队首元素不是当前线程,那么便不会进行锁的争抢
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 如果锁的持有者是当前线程,则可以重复加锁并且由于已经加锁了不会再出现多线程情况,因此不需要多线程控制
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 如果此时没有线程获取锁,那么当前线程可以再次尝试获取锁
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 如果锁的持有者是当前线程,则可以重复加锁并且由于已经加锁了不会再出现多线程情况,因此不需要多线程控制
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false; // 获取锁失败
}
// AbstractQueuedSynchronizer.java
public final void acquire(int arg) {
if (!tryAcquire(arg) && // FairSync 和 UnfairSync 有各自的实现
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 获取锁失败时,才会走到这,将其加入等待队列队尾,并设置唤醒标记给前一个节点
selfInterrupt(); // 进入阻塞状态
}
整个加锁的逻辑
非公平锁:
- 尝试获取锁(没有线程加锁)
- 再次尝试获取锁(没有线程加锁)
- 重复加锁(加锁线程是当前线程)
- 加入等待队列,并将唤醒标记给等待队列的前一个节点
- 自身进入阻塞状态
公平锁:
- 尝试加锁(没有线程加锁,等待队列为空或等待队列对头线程为当前线程)
- 重复加锁(加锁线程是当前线程)
释放锁的逻辑:
- 尝试释放锁
- 释放锁成功后从队头元素开始往后唤醒阻塞的节点,如果后继节点被取消了,那么会从后往前开始唤醒
为什么会从后往前唤醒呢?
这是因为由于 next 指针的不可靠性所决定的,我们来看一下 enq 方法
private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; // 可以多个线程都执行完此方法 if (compareAndSetTail(t, node)) { // 由于 CAS 的存在,只会存在一个线程执行完该方法,最终会导致 多个线程的 prev 指向了 t 。但是 t 的后驱只有一个 t.next = node; return t; } } } }
当处于第二种状态的时候,通过从后往前遍历可以找到线程 1 ,若从从前往后遍历会导致找不到线程 1
参考