AQS 抽象队列同步器,是Java面试的热点问题,比如synchronized和ReentrantLock(基于AQS)之间的区别、AQS队列中节点的状态类型、AQS的加锁、解锁过程等等,这篇文章就简单地分析一下AQS的一个主要子类ReentrantLock。
ReentrantLock
为了深入了解一个底层实现,我们先从它的使用方式开始着手,在实践中我们经常会通过ReentrantLock来进行加锁和解锁,对应的lock和unlock两个方法,其实现如下:
public void lock() { // 获取锁 阻塞
sync.lock(); // 内部包含AQS.acquire(1)
}
public void unlock() { // 释放锁
sync.release(1); // 内部包含AQS.release(1)
}
public boolean tryLock() { // 尝试获取锁 非阻塞 直接返回结果
return sync.nonfairTryAcquire(1);
}
从以上代码可以看出,加锁和解锁的过程都是通过调用sync来实现的,那它是个什么玩意?我们从ReentrantLock的属性和构造函数中可以发现,Snyc是ReentrantLock的一个继承了AQS的静态内部类,并且有两个子类FairSync和NonFairSync。默认情况下ReentrantLock会构造一个非公平的Sync。
private final Sync sync;
public ReentrantLock() { // 空参的实例化方法
sync = new NonfairSync(); // 实例化一个非公平锁
}
public ReentrantLock(boolean fair) { // 指定是否公平
sync = fair ? new FairSync() : new NonfairSync();
}
abstract static class Sync extends AbstractQueuedSynchronizer{...} // 锁的抽象类
static final class NonfairSync extends Sync{...} // 非公平锁
static final class FairSync extends Sync{...} // 公平锁
Sync
我们再深入了解一下Sync内部的具体实现,探究它是如何加锁的:
abstract static class Sync extends AbstractQueuedSynchronizer {
// 阻塞加锁 由子类实现
abstract void lock(); // 由子类去实现具体的加锁过程(公平锁和非公平锁)
// 非阻塞加锁
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) { // 如果可以获取资源
if (compareAndSetState(0, acquires)) { // CAS进行修改
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 如果资源已经被持有,判断为当前线程
int nextc = c + acquires; // 如果是当前线程则更新state
if (nextc < 0) // overflow // 如果超过了Integer的最大值,则抛出异常
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
// 释放锁 公平锁和非公平锁都一样
protected final boolean tryRelease(int releases) { // AQS#release会调用
int c = getState() - releases; // 减 同步状态
if (Thread.currentThread() != getExclusiveOwnerThread())// 不允许没有持有锁还想释放锁的行为,没有线程lock资源时getExclusiveOwnerThread()会返回null
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 由于是可重入锁,状态减为0时才算释放成功
free = true;
setExclusiveOwnerThread(null); // 更新当前的独占锁
}
setState(c); // 更新state
return free;
}
// 是否独占持有锁
protected final boolean isHeldExclusively() {
// While we must in general read state before owner,
// we don't need to do so to check if current thread is owner
return getExclusiveOwnerThread() == Thread.currentThread();
}
final ConditionObject newCondition() {
return new ConditionObject();
}
...
}
NonfairSync和FairSync
从以上代码可以发现,主要的lock方法由其子类NonfairSync和FairSync实现,分别代表了非公平锁和公平锁。
static final class NonfairSync extends Sync { // 非公平锁
final void lock() { // 阻塞式的
if (compareAndSetState(0, 1)) // 非公平锁不管有没有在等待的线程,直接上去拿锁(不讲武德)
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 父类AQS实现
}
protected final boolean tryAcquire(int acquires) { // AQS#acquire会调用
return nonfairTryAcquire(acquires);
}
}
static final class FairSync extends Sync { // 公平锁
final void lock() {
acquire(1); // 父类AQS实现
}
protected final boolean tryAcquire(int acquires) { // AQS#acquire会调用
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 当前资源可以获取,但是需要去看是否还有其他线程在等待(非公平锁直接CAS)
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;
}
}
AQS#acquire(1)和release(1)
在lock方法中可以发现lock中的acquire(1);和unlock中的release(1);这两个方法都是AQS实现的,接下来就继续剖析一下这AQS中的这两个方法:
public final void acquire(int arg) { // Lock#lock使用
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 加锁失败则中断阻塞自己
selfInterrupt();
}
public final boolean release(int arg) { // Lock#unlock使用
if (tryRelease(arg)) { // 如果释放成功
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 需要将后继者唤醒
return true;
}
return false;
}
由以上代码可以看出,在aquire和release操作时,都会先进行一个tryAcquire和tryRelease,这玩意是干啥的?实际上try这个过程是一个非阻塞的操作,相当于检查是否符合加锁lock或解锁unlock的条件(具体的实现在ReentrantLock.Sync的两个子类中):
- tryAcquire:
- 非公平:(如果当前的state==0且CAS修改state为1成功) 或 (state!=0但是当前线程持有锁,修改state即可)
- 公平:(如果当前的state==0且当前没有前驱线程在等待且CAS修改state为1成功)或 (state!=0但是当前线程持有锁,修改state即可)
- tryRelease:非公平和公平都一样,就是检测当前线程是不是当前持有资源的独占线程,然后更新对应的state,如果state==0则返回true,如果state>0代表是可重入锁,当前线程还有其他代码块持有锁,因此返回false
实际上在tryLock和tryRelease的过程中,只是进行了state的更新操作,并没有实际更新队列,那么就需要进一步对这个队列进行操作。AQS 详解 | JavaGuide
- lock时没有竞争到锁,则会加入到等待队列中,并根据前驱节点的状态进行park操作
- unlock时如果释放了锁,则需要从队列中移除并unpark符合条件的后继节点
节点的状态
AQS中的节点是有不同状态的:
| 状态 | 说明 |
|---|---|
| INITIAL | 值为0,初始状态 |
| CANCELLED | 值为1,由于超时或中断,该节点被取消。 节点进入该状态将不再变化。特别是具有取消节点的线程永远不会再次阻塞 |
| SIGNAL | 值为-1,后继节点的线程处于等待状态,而当前节点的线程如果释放了同步状态或者被取消,那么就会通知后继节点,让后继节点的线程能够运行 |
| CONDITION | 值为-2,节点在等待队列中,节点线程等待在Condition上,不过当其他的线程对Condition调用了signal()方法后,该节点就会从等待队列转移到同步队列中,然后开始尝试对同步状态的获取 |
| PROPAGATE | 值为-3,表示下一次的共享式同步状态获取将会无条件的被传播下去 |
AQS加锁过程
我们先分析如果竞争锁失败怎么加入到这个CLH队列中:
public final void acquire(long arg) {
// 如果没有获取锁 则加入等待队列中
// 如果加入等待队列失败则中断自己
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
关键在于acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中的addWaiter和acquireQueued,先分析addWaiter:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); // 创建一个节点,包含当前线程的指针和后继节点为null
// Try the fast path of enq; backup to full enq on failure
Node pred = tail; // 当前节点的前驱节点为CLH队列的队尾
if (pred != null) {
node.prev = pred; // 先将当前元素的前驱节点设置为prev
if (compareAndSetTail(pred, node)) { // CAS更新CLH的尾节点
pred.next = node; // 更新前驱节点的后继节点
return node;
}
}
enq(node); // CLH没有初始化 或者 有其他线程抢先把自己链到尾部称为新尾部了 那么就会进行enq的操作
return node;
}
private Node enq(final Node node) {
for (;;) { // 一直循环入队
Node t = tail; // 得到当前的尾部
if (t == null) { // 如果队列没有初始化,没有尾部,则进行初始化
if (compareAndSetHead(new Node()))
tail = head;
} else { // 否则持续找当前最新的尾部,把自己加上去
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
从以上代码可以看出,addWaiter就是新创建一个节点(包含当前线程的指针)将其CAS加入到尾部,并且返回这个节点
接下来分析acquireQueued:
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)) { // 如果前驱节点为head并且当前具备持有锁的条件
setHead(node);
p.next = null; // help GC 将之前的头节点指向null
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
acquireQueued会先判断当前传入的Node对应的前置节点是否为head,如果是则尝试去持有锁。持有锁成功则将当前节点设置为head节点,然后删除之前的head节点。
如果加锁失败或者Node的前置节点不是head节点,就会通过shouldParkAfterFailedAcquire方法将head节点的waitStatus变成SIGNAL=-1,最后执行parkAndChecknIterrupt方法,调用LockSupport.park() 挂起当前线程。此时当前线程需要等待其他线程释放锁来唤醒。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus; // 得到前置节点的等待状态
if (ws == Node.SIGNAL) // 如果是SIGNAL状态,则放心地park自己
return true;
if (ws > 0) { // 如果前驱节点是CANCELLED状态,则需要一直向前遍历前,将CANCELLED状态的节点删除
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else { // 将前驱节点的等待状态变更为Node.SIGNAL,这样前驱节点释放锁时需要唤醒后继节点
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() { // 通过该方法park自己
LockSupport.park(this);
return Thread.interrupted();
}
AQS解锁过程
当线程持有锁时,指向它的节点肯定是头节点,那么现在释放锁的话就需要判断是否需要唤醒后继节点,主要通过unparkSuccessor方法进行唤醒操作。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
当当前头节点不处于初始状态时,代表它需要unpark后继节点:
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0) // CAS更新当前节点的状态,使其使出初始状态
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) { // 如果下一个节点为null或者处于cancelled状态
s = null;
for (Node t = tail; t != null && t != node; t = t.prev) // 从尾节点向前遍历,找到head后面第一个不处于canceled状态的节点
if (t.waitStatus <= 0) // 不断更新s的值
s = t;
}
if (s != null) // 找到符合条件的线程则unpark它
LockSupport.unpark(s.thread);
}
在以上代码中有一个值得注意的点,就是unparkSuccessor为什么从后往前遍历找第一个符合条件的线程唤醒,对应的代码为for (Node t = tail; t != null && t != node; t = t.prev),可以看出是从tail开始向前遍历的。
unparkSuccessor为什么从后往前遍历找第一个符合条件的线程唤醒
# Java AQS unparkSuccessor 方法中for循环为什么是从tail开始而不是head
这就和入队的插入顺序有关了,在我们竞争锁没有竞争过的时候需要创建一个节点,并将它置于CLH队列中。在该段方法中,将当前节点置于尾部使用了CAS来保证线程安全,但是在if语句块中的代码并没有使用任何手段来保证线程安全!在高并发情况下,可能会出现这种情况:线程A通过CAS进入if语句块之后,发生上下文切换,此时线程B同样执行了该方法,并且执行完毕。然后线程C调用了unparkSuccessor方法。就是还没来得及设置之前tail的后继节点就进行上下文切换了,而当前t.next还是为null,因此会出现后续节点被漏掉的情况。
总的来说,其最根本的原因在于:node.prev = t;先于CAS执行,也就是说,你在将当前节点置为尾部之前就已经把前驱节点赋值了,自然不会出现prev=null的情况
private Node enq(final Node node) {
for (;;) { // 一直循环入队
Node t = tail; // 得到当前的尾部
if (t == null) { // 如果队列没有初始化,没有尾部,则进行初始化
if (compareAndSetHead(new Node()))
tail = head;
} else { // 否则持续找当前最新的尾部,把自己加上去
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}