概述
AbstractQueuedSynchronizer,队列同步器,简称AQS,它是java并发用来构建锁或者其他同步组件的基础框架。
事实上从下图看出很多并发组建确实都使用了AQS实现同步需求,学习并发编程,AQS是基础中的基础,重点中的重点。
1 同步器的接口
同步器的设计基于模版方法模式,使用者继承同步器,重写指定的方法,并调用同步器提供的模版方法。
同步状态变量相关方法:
- protected final int getState(); 获取当前同步状态
- protected final void setState(int newState); 设置当前同步状态
- protected final boolean compareAndSetState(int expect, int update); CAS设置当前同步状态,保证原子更新。
独占式和共享式同步状态的区别如下
AQS提供的重写方法
| 可重写方法名称 | 描述 |
|---|---|
| protected boolean tryAcquire(int arg) | 独占式获取同步状态,该方法的实现需要先查询当前的同步状态是否可以获取,如果可以获取再进行获取; |
| protected boolean tryRelease(int arg) | 独占式释放同步状态 |
| protected int tryAcquireShared(int arg) | 共享式获取同步状态 |
| protected boolean tryReleaseShared(int arg) | 共享式释放状态 |
| protected boolean isHeldExclusively() | 独占模式下,判断同步状态是否已经被占用 |
我们随便找一个可重写方法,发现其默认实现是抛出了一个异常。⾃定义的同步组件或者锁不可能既是独占式⼜是共享式,为了避免强制重写不相⼲⽅法,所以就没有用abstract来修饰了,但要抛出异常告知不能直接使⽤该⽅法:
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
我们继承AQS,重写以上方法时,需要调用AQS提供的模版方法。
| 模版方法名称 | 描述 |
|---|---|
| acquire(int arg) | 独占式获取同步状态。如果当前线程获取同步状态成功,则由该方法返回;否则,将会进入同步队列等待。该方法将会调用可重写的 tryAcquire(int arg) 方法 |
| acquireInterruptibly(int arg) | 与acquire(int arg) 相同,但是该方法响应中断。当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException 异常并返回 |
| tryAcquireSharedNanos(int arg, long nanosTimeout) | 在acquireInterruptibly(int arg)基础上增加了超时限制 |
| tryAcquireNanos(int arg, long nanos) | 超时获取同步状态。如果当前线程在 nanos 时间内没有获取到同步状态,那么将会返回 false ,已经获取则返回 true |
| acquireShared(int arg) | 共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态 |
| acquireSharedInterruptibly(int arg) | 共享式获取同步状态,与acquireShared相同不过响应中断 |
| release(int arg) | 独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒 |
| releaseShared(int arg) | 共享式释放同步状态 |
| Collection getQueuedThreads() | 获取等待在同步队列上的线程集合 |
上述方法都由final修饰,表示这些模版方法只能被调用不能被重写。
如果我们需要自定义互斥锁, 应该实现Lock接口,聚合自定义队列同步器,同时自定义队列同步器继承AQS。
自定义独占锁的代码实例如下
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
public class MyMutex implements Lock, Serializable {
// 静态内部类,继承 AQS 并重写其中方法
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// Acquire the lock if state is zero 当状态为0时获取到锁
public boolean tryAcquire(int acquires) {
assert acquires == 1; // Otherwise unused
if (compareAndSetState(0, 1)) { // 通过 CAS 设置
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// Release the lock by setting state to zero 释放锁,将状态设置为0
protected boolean tryRelease(int releases) {
assert releases == 1; // Otherwise Unused
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// Providers a Condition
Condition newCondition() { return new ConditionObject(); }
}
// 将操作代理到 Sync 上
private final Sync sync = new Sync();
public void lock() { sync.acquire(1); }
public boolean tryLock() { return sync.tryAcquire(1); }
public void unlock() { sync.release(1); }
public Condition newCondition() { return sync.newCondition(); }
public boolean isLocked() { return sync.isHeldExclusively();}
public boolean hasQueuedThreads() { return sync.hasQueuedThreads();}
public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); }
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(timeout)); }
}
2 同步器实现分析
上文我们知道lock.tryLock()其实就是调用了自定义同步器重写的tryAcquire()方法。
2.1 同步队列
AQS底层的数据结构是CLH变体的虚拟双向队列,这个队列遵循FIFO。AQS利用该同步队列管理同步状态。
- 当线程获取同步状态失败时,就会将当前线程以及等待状态等信息构造成⼀个Node 节点,将其加⼊到同步队列中尾部,阻塞该线程
- 当同步状态被释放时,会唤醒同步队列中“⾸节点”的线程获取同步状态
队列中节点几个方法和属性值的含义:
| 方法和属性值 | 含义 |
|---|---|
| waitStatus | 当前节点在队列中的状态 |
| thread | 表示处于该节点的线程 |
| prev | 前驱指针 |
| predecessor | 返回前驱节点,没有的话抛出 npe |
| nextWaiter | 指向下一个处于 CONDITION 状态的节点(由于本篇文章不讲述 Condition Queue 队列,这个指针不多介绍) |
| next | 后继指针 |
线程两种锁的模式:
| 模式 | 含义 |
|---|---|
| SHARED | 表示线程以共享的模式等待锁 |
| EXCLUSIVE | 表示线程正在以独占的方式等待锁 |
waitStatus 有下面几个枚举值:
| 枚举 | 含义 |
|---|---|
| 0 | 当一个 Node 被初始化的时候的默认值 |
| CANCELLED | 为 1,表示线程获取锁的请求已经取消了 |
| CONDITION | 为-2,表示节点在等待队列中,节点线程等待唤醒 |
| PROPAGATE | 为-3,当前线程处在 SHARED 情况下,该字段才会使用 |
| SIGNAL | 为-1,表示线程已经准备好了,就等资源释放了 |
队列的首节点是获取同步状态成功的节点,当一个线程获取到同步状态,其他节点被阻塞并以CAS的方式线程安全地加入队列。
首节点的线程在执行完相关的方法后释放同步状态时,将会唤醒后继节点,后继节点在成功获取到同步状态时将自己设置为首节点。
2.2 同步状态的获取和释放
acquire(int arg)是AQS独占式获取同步状态的模版方法,下文若无特殊说明,源码都是从java8版本拷贝来的。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
⾸先,尝试⾮阻塞的获取同步状态,如果获取失败(tryAcquire返回false),则会调⽤ addWaiter ⽅法构造 Node 节点(Node.EXCLUSIVE 独占式)并安全的(CAS)加⼊到同步队列【尾部】,最后调用acquireQueued使得该节点以 死循环(自旋)的方式获取同步状态。
//这个函数比较简单,就是将node放到队列末尾,mode表示是独占锁还是共享锁以后再讨论
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {//如果tail不是null,表示队列已被初始化,尝试快速在尾部添加当前节点
node.prev = pred;
if (compareAndSetTail(pred, node)) {
//cas将tail加到队尾,如果失败走到enq函数继续cas+自旋到队尾
pred.next = node;
return node;
}
}
enq(node); //初始化队列或者再次cas队尾
return node;
}
//该函数会初始化队列(如果队列未被初始化)
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队尾,如果还失败看到这个是死循环会一直去放,直到放到队尾为止
t.next = node;
return t;
}
}
}
}
获取同步状态失败的节点被正确添加到队尾后,会进行死循环不断尝试获取同步状态,这种死循环不断乐观尝试的过程我们称之为自旋。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;//标记是否成功拿到资源
try {
boolean interrupted = false;//标记等待过程中是否被中断过
//又是一个“自旋”!
for (;;) {
final Node p = node.predecessor();//拿到前驱
//如果前驱是head,节点为老二,尝试获取同步状态。
if (p == head && tryAcquire(arg)) {
setHead(node); //获取同步状态后,将head指向该结点。
p.next = null; // 上一个获取同步状态节点(哨兵节点)指向空便于gc
failed = false; // 成功获取资源
return interrupted; //返回等待过程中是否被中断过
}
//如果自己可以休息了则通过park()进入waiting状态,直到被unpark()。如果不可中断的情况下被中断了,那么会从park()中醒过来,发现拿不到资源,从而继续进入park()等待。
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
//如果等待过程中被中断过,哪怕只有那么一次,就将interrupted标记为true
}
} finally {
if (failed)
// 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了)
// 那么取消结点在队列中的等待。
cancelAcquire(node);
}
}
shouldParkAfterFailedAcquire(p, node)和parkAndCheckInterrupt()就会将线程获取同步状态失败的线程挂起,避免获取同步状态失败的陷入“死循环”浪费资源。
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;//拿到前驱的状态
if (ws == Node.SIGNAL)
//如果已经告诉前驱拿完号后通知自己一下,那就可以安心休息了
return true;
if (ws > 0) {
/*
* 如果前驱放弃了,那就一直往前找,直到找到最近一个正常等待的状态,并排在它的后边。
* 注意:那些放弃的结点,由于被自己“加塞”到它们前边,它们相当于形成一个无引用链,稍后就会被保安大叔赶走了(GC回收)!
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
//如果前驱正常,那就把前驱的状态设置成SIGNAL,告诉它拿完号后通知自己一下。
// 有可能失败,人家说不定刚刚释放完呢!
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// 如果前驱节点的 waitStatus 是 SIGNAL状态,即 shouldParkAfterFailedAcquire ⽅法会返回 true
// 程序会继续向下执⾏ parkAndCheckInterrupt ⽅法,⽤于将当前线程挂起
private final boolean parkAndCheckInterrupt() {
// 线程挂起,程序不会继续向下执⾏; 当前线程进入waiting状态
LockSupport.park(this);
// 根据 park ⽅法 API描述,程序在下述三种情况会继续向下执⾏
//1.被 unpark
//2. 被中断(interrupt)
//3. 其他不合逻辑的返回才会继续向下执⾏
// 因上述三种情况程序执⾏⾄此,返回当前线程的中断状态(查看是否被中断),并清空中断状态
// 如果由于被中断,该⽅法会返回 true
return Thread.interrupted();
}
被唤醒的程序会继续执⾏acquireQueued⽅法⾥的循环(自旋),如果获取同步状态成功,则会返回 interrupted = true的结果。最终回到最上层的方法。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt(); // 由于之前中断状态被清空,需要重置中断标识
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
中断是一种【协同】机制,怎么理解这么高大上的词呢?就是女朋友叫你吃饭,你收到了中断游戏通知,但是否⻢上放下手中的游戏去吃饭看你心情 。在程序中怎样演绎这个心情就看具体的业务逻辑了,Java 的中断机制就是这么简单
2.2.1 取消等待
正常情况下,如果跳出循环,failed 的值为false,所以只有不正常的情况才会执行到这里,也就是会发生异常,才会执行到此处。
final boolean acquireQueued(final Node node, int arg) {
......
finally {
if (failed)
// 如果等待过程中没有成功获取资源(如timeout,或者可中断的情况下被中断了)
// 那么取消结点在队列中的等待。
cancelAcquire(node);
}
}
查看 try 代码块,只有两个方法会抛出异常:
- node.processor() 方法
- 自己重写的 tryAcquire()
前驱节点这个方法返回的空显然不是我们要关心的(不会出现这种情况)
/**
* Returns previous node, or throws NullPointerException if null.
* Use when predecessor cannot be null. The null check could
* be elided, but is present to help the VM.
* @return the predecessor of this node
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
因此我们把目光转向自定义同步器重写的tryAcquire()方法,这里以ReentrantLock.FairSync.tryAcquire()为例。我们看到抛出了 Maximum lock count exceeded的Error,这种情况下会取消在队列中排队等待获取同步状态。
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;
}
另外,上面分析 shouldParkAfterFailedAcquire 方法还对 CANCELLED 的状态进行了判断,CANCELLED状态的阶段会被从等待队列中清除。
那么问题又来了, 节点清楚具体是什么做的,我们从 cancelAcquire() 这个方法的源码中寻找答案。
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist 忽略无效节点
if (node == null)
return;
// 将关联的线程信息清空
node.thread = null;
// Skip cancelled predecessors 跳过同样是取消状态的前驱节点
Node pred = node.prev;
while (pred.waitStatus > 0)
//waitStatus>0表示该节点是被取消的节点
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
// 跳出上面循环后找到前驱有效节点 pred,并获取该有效节点的后继节点predNext
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
// 将当前节点的状态置为 CANCELLED
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
// 情况1,当前节点是尾节点,那么直接从队尾将自己删除
if (node == tail && compareAndSetTail(node, pred)) {
// 队尾是前驱有效节点 pred,predNext=null
compareAndSetNext(pred, predNext, null);
} else {
//进入else说明node不是队尾(或者是队尾但是cas队尾失败(其实结果也不是队尾,因为被别的线程抢先了))
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
// 情况2
// 1. 如果当前节点的有效前驱节点不是头节点,也就是说当前节点不是头节点的后继节点
if (pred != head &&
// 2. 判断当前节点有效前驱节点的状态是否为 SIGNAL
((ws = pred.waitStatus) == Node.SIGNAL ||
// 3. 如果不是,尝试将前驱节点的状态置为 SIGNAL
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
// 判断当前节点有效前驱节点的线程信息是否为空
pred.thread != null) {
// 上述条件满足
Node next = node.next;
// 将当前节点有效前驱节点的后继节点指针指向当前节点的后继节点
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 情况3,如果当前节点的前驱节点是头节点,或者上述其他条件不满足,就唤醒当前节点的后继节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
其核心目的就是从等待队列中移除 CANCELLED 的节点,并重新拼接整个队列,总结来看,其实设置 CANCELLED 状态节点只是有三种情况,我们通过画图来分析一下。
至此,获取同步状态的过程就结束了,我们简单的用流程图说明一下整个过程。
2.3 独占式释放同步状态
AQS 模版方法 release() 是入口
public final boolean release(int arg) {
// 调用自定义同步器重写的 tryRelease 方法尝试释放同步状态
if (tryRelease(arg)) {
// 释放成功,获取头节点
Node h = head;
// 存在头节点,并且waitStatus不是初始状态
// 通过获取的过程我们已经分析了,在获取的过程中会将 waitStatus的值从初始状态更新成 SIGNAL 状态
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
// 获取头节点的waitStatus
int ws = node.waitStatus;
if (ws < 0)
// 清空头节点的waitStatus值,即置为0
compareAndSetWaitStatus(node, ws, 0);
// 获取头节点的后继节点
Node s = node.next;
// 判断当前节点的后继节点是否是取消状态,如果是,需要移除,重新连接队列
// s.waitStatus > 0 是取消状态
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);
}