简单介绍
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
什么是AQS?
什么是AQS?
java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。
AQS全称为AbstractQueuedSynchronizer, 存在于java.util.concurrent.locks包下面。
顾名思义,AQS是一个用来构建锁和同步器的框架,使用AQS能简单且高效地构造出应用广泛的大量的同步器。
比如我们提到的ReentrantLock,Semaphore,其他的诸如ReentrantReadWriteLock,SynchronousQueue,FutureTask等等皆是基于AQS的。当然,我们自己也能利用AQS非常轻松容易地构造出符合我们自己需求的同步器。
AQS原理
AQS核心思想
AQS控制线程同步的核心思想
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套「线程阻塞等待以及被唤醒时锁分配的机制」。
这个机制 AQS 是用 CLH 队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
-
什么是
CLH队列?CLH(Craig,Landin and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
结合我们上面的核心思想概述,以及CLH的结构画出如下原理图:
AQS 使用一个 int 成员变量来表示同步状态,通过内置的 FIFO 队列(CLH)来完成获取资源线程的排队工作。AQS 使用 CAS对该同步状态进行原子操作实现对其值的修改。
private volatile int state; // 共享变量,使用volatile关键字修饰保证线程可见性
AQS对资源的共享方式
AQS定义两种资源共享方式
-
Exclusive(独占):只有一个线程能执行,如
ReentrantLock。又可分为公平锁和非公平锁:- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
-
Share(共享):多个线程可同时执行,如
CountDownLatch、Semaphore、CyclicBarrier、ReadWriteLock我们都会在后面讲到。
关于独占/共享锁,公平/非公平锁的内容我在博客ReentrantLock及源码分析中有提到。
自定义同步器时需要注意什么?需要实现什么?
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS 已经在顶层实现好了。
-
独占锁的实现
以
ReentrantLock为例,state 初始化为 0,表示未锁定状态。 A 线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到 A 线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。
-
共享锁的实现
再以
CountDownLatch以例,任务分为 N 个子线程去执行,state 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后
countDown()一次,state 会CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
AQS底层设计--模板方法模式
同步器的设计和模板方法模式的关系
同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用):
- 使用者继承
AbstractQueuedSynchronizer并重写指定的方法。(这些重写方法很简单,无非是对于共享资源 state 的获取和释放) - 将
AQS组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。
-
这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。具体是什么区别呢?
AQS使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法:protected boolean tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。 protected boolean tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。 protected int tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 protected boolean tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。 protected boolean isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。 -
什么是钩子方法?
钩子方法是一种被声明在抽象类中的方法,它可以是空方法(由子类实现),也可以是默认实现的方法。
模板设计模式通过钩子方法控制固定步骤的实现。
除了上面提到的钩子方法之外,
AQS类中的其他方法都是final,所以无法被其他类重写。
一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。
但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。
AQS源码分析
内部类Node
内部类Node
static final class Node {
// 模式,分为共享与独占
// 共享模式
static final Node SHARED = new Node();
// 独占模式
static final Node EXCLUSIVE = null;
// 结点状态
// CANCELLED,值为1,表示当前的线程被取消
// SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark
// CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中
// PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行
// 值为0,表示当前节点在sync队列中,等待着获取锁
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
// 结点状态
volatile int waitStatus;
// 前驱结点
volatile Node prev;
// 后继结点
volatile Node next;
// 结点所对应的线程
volatile Thread thread;
// 下一个等待者
Node nextWaiter;
// 结点是否在共享模式下等待
final boolean isShared() {
return nextWaiter == SHARED;
}
// 获取前驱结点,若前驱结点为空,抛出异常
final Node predecessor() throws NullPointerException {
// 保存前驱结点
Node p = prev;
if (p == null) // 前驱结点为空,抛出异常
throw new NullPointerException();
else // 前驱结点不为空,返回
return p;
}
// 无参构造方法
Node() { // Used to establish initial head or SHARED marker
}
// 构造方法
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
// 构造方法
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
每个线程被阻塞的线程都会被封装成一个Node结点,放入队列。 每个节点包含了一个Thread类型的引用,并且每个节点都存在一个状态,具体状态如下。
-
CANCELLED,值为1,表示当前的线程被取消。作废状态,该节点的线程由于超时,中断等原因而处于作废状态。是不可逆的,一旦处于这个状态,说明应该将该节点移除了。
-
SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,需要进行unpark操作。待唤醒后继状态,当前节点的线程处于此状态,后继节点会被挂起,当前节点释放锁或取消之后必须唤醒它的后继节点。
-
CONDITION,值为-2,表示当前节点在等待condition,也就是在condition queue中。等待状态,表明节点对应的线程因为不满足一个条件(
Condition)而被阻塞。 -
PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行。 -
当值为0,表示当前节点在
sync queue中,等待着获取锁。
内部类ConditionObject
内部类
ConditionObject
以下代码只展示主要方法的方法体:
// 内部类
public class ConditionObject implements Condition, java.io.Serializable {
// 版本号
private static final long serialVersionUID = 1173984872572414699L;
// condition队列的头节点
private transient Node firstWaiter;
// condition队列的尾结点
private transient Node lastWaiter;
// 构造方法
public ConditionObject() { }
// private methods
// 添加新的waiter到wait队列
private Node addConditionWaiter() {
// 保存尾结点
Node t = lastWaiter;
if (t != null && t.waitStatus != Node.CONDITION) { // 尾结点不为空,并且尾结点的状态不为CONDITION
// 清除状态为CONDITION的结点
unlinkCancelledWaiters();
// 将最后一个结点重新赋值给t
t = lastWaiter;
}
// 新建一个结点
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null) // 尾结点为空
// 设置condition队列的头节点
firstWaiter = node;
else // 尾结点不为空
// 设置为节点的nextWaiter域为node结点
t.nextWaiter = node;
// 更新condition队列的尾结点
lastWaiter = node;
return node;
}
private void doSignal(Node first) {
// 循环
do {
if ( (firstWaiter = first.nextWaiter) == null) // 该节点的nextWaiter为空
// 设置尾结点为空
lastWaiter = null;
// 设置first结点的nextWaiter域
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null); // 将结点从condition队列转移到sync队列失败并且condition队列中的头节点不为空,一直循环
}
private void doSignalAll(Node first) {
// condition队列的头节点尾结点都设置为空
lastWaiter = firstWaiter = null;
// 循环
do {
// 获取first结点的nextWaiter域结点
Node next = first.nextWaiter;
// 设置first结点的nextWaiter域为空
first.nextWaiter = null;
// 将first结点从condition队列转移到sync队列
transferForSignal(first);
// 重新设置first
first = next;
} while (first != null);
}
// 从condition队列中清除状态为CANCEL的结点
private void unlinkCancelledWaiters() {
// 保存condition队列头节点
Node t = firstWaiter;
Node trail = null;
while (t != null) { // t不为空
// 下一个结点
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) { // t结点的状态不为CONDTION状态
// 设置t节点的nextWaiter域为空
t.nextWaiter = null;
if (trail == null) // trail为空
// 重新设置condition队列的头节点
firstWaiter = next;
else // trail不为空
// 设置trail结点的nextWaiter域为next结点
trail.nextWaiter = next;
if (next == null) // next结点为空
// 设置condition队列的尾结点
lastWaiter = trail;
}
else // t结点的状态为CONDTION状态
// 设置trail结点
trail = t;
// 设置t结点
t = next;
}
}
// public methods
// 唤醒一个等待线程。如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取锁。
public final void signal() {
if (!isHeldExclusively()) // 不被当前线程独占,抛出异常
throw new IllegalMonitorStateException();
// 保存condition队列头节点
Node first = firstWaiter;
if (first != null) // 头节点不为空
// 唤醒一个等待线程
doSignal(first);
}
// 唤醒所有等待线程。如果所有的线程都在等待此条件,则唤醒所有线程。在从 await 返回之前,每个线程都必须重新获取锁。
public final void signalAll() {
if (!isHeldExclusively()) // 不被当前线程独占,抛出异常
throw new IllegalMonitorStateException();
// 保存condition队列头节点
Node first = firstWaiter;
if (first != null) // 头节点不为空
// 唤醒所有等待线程
doSignalAll(first);
}
// 等待,当前线程在接到信号之前一直处于等待状态,不响应中断
public final void awaitUninterruptibly() {
// 添加一个结点到等待队列
Node node = addConditionWaiter();
// 获取释放的状态
int savedState = fullyRelease(node);
boolean interrupted = false;
while (!isOnSyncQueue(node)) { //
// 阻塞当前线程
LockSupport.park(this);
if (Thread.interrupted()) // 当前线程被中断
// 设置interrupted状态
interrupted = true;
}
if (acquireQueued(node, savedState) || interrupted) //
selfInterrupt();
}
/** Mode meaning to reinterrupt on exit from wait */
private static final int REINTERRUPT = 1;
/** Mode meaning to throw InterruptedException on exit from wait */
private static final int THROW_IE = -1;
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
private void reportInterruptAfterWait(int interruptMode)
// ...
}
// 等待,当前线程在接到信号或被中断之前一直处于等待状态
public final void await() throws InterruptedException {
if (Thread.interrupted()) // 当前线程被中断,抛出异常
throw new InterruptedException();
// 在wait队列上添加一个结点
Node node = addConditionWaiter();
//
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 阻塞当前线程
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) // 检查结点等待时的中断类型
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
// 等待,当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态
public final long awaitNanos(long nanosTimeout)
throws InterruptedException {
// ...
}
// 等待,当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态
public final boolean awaitUntil(Date deadline)
throws InterruptedException {
// ...
}
// 等待,当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。此方法在行为上等效于: awaitNanos(unit.toNanos(time)) > 0
public final boolean await(long time, TimeUnit unit)
throws InterruptedException {
// ...
}
// support for instrumentation
final boolean isOwnedBy(AbstractQueuedSynchronizer sync) {
return sync == AbstractQueuedSynchronizer.this;
}
// 查询是否有正在等待此条件的任何线程
protected final boolean hasWaiters() {
// ...
}
// 返回正在等待此条件的线程数估计值
protected final int getWaitQueueLength() {
// ...
}
// 返回包含那些可能正在等待此条件的线程集合
protected final Collection<Thread> getWaitingThreads() {
// ...
}
}
此类实现了Condition接口,Condition接口定义了条件操作规范,具体如下:
public interface Condition {
// 等待,当前线程在接到信号或被中断之前一直处于等待状态
void await() throws InterruptedException;
// 等待,当前线程在接到信号之前一直处于等待状态,不响应中断
void awaitUninterruptibly();
//等待,当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态
long awaitNanos(long nanosTimeout) throws InterruptedException;
// 等待,当前线程在接到信号、被中断或到达指定等待时间之前一直处于等待状态。此方法在行为上等效于: awaitNanos(unit.toNanos(time)) > 0
boolean await(long time, TimeUnit unit) throws InterruptedException;
// 等待,当前线程在接到信号、被中断或到达指定最后期限之前一直处于等待状态
boolean awaitUntil(Date deadline) throws InterruptedException;
// 唤醒一个等待线程。如果所有的线程都在等待此条件,则选择其中的一个唤醒。在从 await 返回之前,该线程必须重新获取锁。
void signal();
// 唤醒所有等待线程。如果所有的线程都在等待此条件,则唤醒所有线程。在从 await 返回之前,每个线程都必须重新获取锁。
void signalAll();
}
看完了源码接下来我们解决几个问题
ConditionObject是什么?有什么用?
ConditionObject实现了Condition接口。
Condition用于线程间的通信,能够把锁粒度减小。
我们在ReentrantLock及源码分析中有说明Condition的用法,主要就是代替wait/noitfy的线程同步方式。
这个内部类还维护了一个condition队列,而且Node.nextWaiter就是用来将condition连接起来的。
//condition队头
private transient Node firstWaiter;
//condition队尾
private transient Node lastWaiter;
//发生了中断,但在后续不抛出中断异常,而是“补上”这次中断
private static final int REINTERRUPT = 1;
//发生了中断,且在后续需要抛出中断异常
private static final int THROW_IE = -1;
ConditionObject类中,重点是await()和signal()两个方法。这两个方法我们都将一一解释。
await方法
await方法的流程
当前线程处于阻塞状态,直到调用signal()或中断才能被唤醒。代码如下:
public final void await() throws InterruptedException {
//如果被中断,就处理中断异常
if (Thread.interrupted())
throw new InterruptedException();
//初始化链表的功能,设置当前线程为链尾
Node node = addConditionWaiter();
//释放当前节点持有的所有资源
int savedState = fullyRelease(node);
int interruptMode = 0;
//如果当前线程不在等待队列中,
//说明此时一定在条件队列里,将其阻塞。
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
//说明中断状态发生变化
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
//当前线程执行了signal方法会经过这个,也就是重新将当前线程加入同步队列中
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
//删除条件队列中不满足要求的元素
if (node.nextWaiter != null)
unlinkCancelledWaiters();
//处理被中断的情况
if (interruptMode != 0)
//如果是THROW_IE,说明signal之前发生中断
//如果是REINTERRUPT,signal之后中断,
//所以成功获取资源后会调用selfInterrupt()
reportInterruptAfterWait(interruptMode);
}
- (1) 将当前线程封装成node且等待状态为
CONDITION。 - (2) 释放当前线程持有的所有资源,让下一个线程能获取资源。
- (3) 加入到条件队列后,则阻塞当前线程,等待被唤醒。
- (4) 如果是因
signal被唤醒,则节点会从条件队列转移到等待队列;如果是因中断被唤醒,则记录中断状态。两种情况都会跳出循环。 - (5) 若是因
signal被唤醒,就自旋获取资源;否则处理中断异常。
接着我们看addConditionWaiter方法
-
addConditionWaiter方法private Node addConditionWaiter() { Node t = lastWaiter; // 判断队尾元素,如果非条件等待状态则清理出去 if (t != null && t.waitStatus != Node.CONDITION) { unlinkCancelledWaiters(); //可能t之前引用的节点被删除了,所以要重新引用 t = lastWaiter; } //这个节点就表示当前线程 Node node = new Node(Thread.currentThread(), Node.CONDITION); //说明条件按队列中没有元素 if (t == null) firstWaiter = node; else t.nextWaiter = node; lastWaiter = node; return node; }将当前线程封装成节点,添加到条件队列尾部,并返回当前节点。
接着我们看unlinkCancelledWaiters方法
-
unlinkCancelledWaiters方法private void unlinkCancelledWaiters() { Node t = firstWaiter; //记录在循环中上一个waitStatus有效的节点 Node trail = null; while (t != null) { Node next = t.nextWaiter; //再次判断等待状态,保证节点都是CONDITION状态 //确保当前节点无效后删除引用 if (t.waitStatus != Node.CONDITION) { t.nextWaiter = null; if (trail == null) firstWaiter = next; else //否则就直接加到队尾的后面 trail.nextWaiter = next; if (next == null) lastWaiter = trail; } else //记录有效的节点并向后遍历 trail = t; t = next; } }遍历一遍条件队列,删除非
CONDITION状态的节点。
signal方法
signal方法流程
唤醒一个被阻塞的线程
public final void signal() {
//判断当前线程是否为资源的持有者
//这也是必须在lock()与unlock()代码中间执行的原因
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//开始唤醒条件队列的第一个节点
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
- (1) 判断当前线程是否为资源的持有者,如果不是则抛出异常
- (2) 唤醒条件队列的第一个节点
接着我们看doSignal方法
-
doSignal方法private void doSignal(Node first) { do { //后续的等待条件为空,说明condition队列中只有一个节点 if ( (firstWaiter = first.nextWaiter) == null) lastWaiter = null; first.nextWaiter = null; //transferForSignal()是真正唤醒头节点的地方 } while (!transferForSignal(first) && (first = firstWaiter) != null); }将条件队列的头节点从条件队列转移到等待队列,并且,将该节点从条件队列删除。
接着我们看
transferForSignal方法-
transferForSignal方法final boolean transferForSignal(Node node) { //当前节点等待状态改变失败,则说明当前节点不是CONDITION //状态,那就不能进行接下来的操作,返回false //0是正常状态 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; }将节点放入等待队列并唤醒。并不需要在条件队列中移除,因为条件队列每次插入时都会把状态不为
CONDITION的节点清理出去。
-
类的属性
类的属性
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
// 版本号
private static final long serialVersionUID = 7373984972572414691L;
// 头节点
private transient volatile Node head;
// 尾结点
private transient volatile Node tail;
// 状态
private volatile int state;
// 自旋时间
static final long spinForTimeoutThreshold = 1000L;
// Unsafe类实例
private static final Unsafe unsafe = Unsafe.getUnsafe();
// state内存偏移地址
private static final long stateOffset;
// head内存偏移地址
private static final long headOffset;
// state内存偏移地址
private static final long tailOffset;
// tail内存偏移地址
private static final long waitStatusOffset;
// next内存偏移地址
private static final long nextOffset;
// 静态初始化块
static {
try {
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
headOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("head"));
tailOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));
waitStatusOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("waitStatus"));
nextOffset = unsafe.objectFieldOffset
(Node.class.getDeclaredField("next"));
} catch (Exception ex) { throw new Error(ex); }
}
}
属性中包含了:
-
头节点
head,尾结点tail这个就是我们在「
AQS核心思想」中提及的CLH队列(同步队列)的头尾节点CLH队列由链表实现,以自旋的方式获取资源,是可阻塞的先进先出的双向队列。(这点可以重新回顾「内部类Node」)通过自旋和
CAS操作保证节点插入和移除的原子性。当有线程获取锁失败,就被添加到队列末尾。 -
状态
state(锁状态表示)state是临界资源,也是锁的描述。表示有多少线程获取了锁。 -
自旋时间
spinForTimeoutThreshold -
还有
AbstractQueuedSynchronizer抽象的属性在内存中的偏移地址通过该偏移地址,可以获取和设置该属性的值,同时还包括一个静态初始化块,用于加载内存偏移地址。
acquire方法
acquire--独占模式获取资源
该方法以独占模式获取(资源),忽略中断,即线程在aquire过程中,中断此线程是无效的。源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
//让线程处于一种自旋状态,
//尝试让该线程重新获取锁!当条件满足获取到了锁则可以从自旋过程中
//退出,否则继续。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
由上述源码可以知道,当一个线程调用acquire时,调用方法流程如下:
我们对acqure的过程先进行简单的介绍,后面再依次对其中的各个方法源码进行分析:
-
(1) 首先调用
tryAcquire方法,调用此方法的线程会试图在独占模式下获取对象状态。此方法应该查询是否允许它在独占模式下获取对象状态,如果允许,则获取它。在
AbstractQueuedSynchronizer源码中默认会抛出一个异常,即需要子类去重写此方法完成自己的逻辑。之后会进行分析。 -
(2) 若
tryAcquire失败,则调用addWaiter方法,addWaiter方法完成的功能是将调用此方法的线程封装成为一个结点并放入Sync queue队尾,并且标记为独占模式。 -
(3) 插入等待队列后(
addWaiter),并没有放弃获取资源。调用acquireQueued方法,此方法完成的功能是Sync queue中的结点不断尝试获取资源,根据前置节点的状态判断是否应该继续获取资源(如果前驱是头节点,继续尝试获取资源),若成功,则返回true,否则,返回false。 -
(4) 在每一次自旋获取资源过程中(
acquireQueued),失败后调用shouldParkAfterFailedAcquire(Node, Node)检测当前节点是否应该park()。若返回true,则调用parkAndCheckInterrupt()中断当前节点中的线程。若返回false,则接着自旋获取资源。 -
(5) 在进行是否需要挂起的判断中(
shouldParkAfterFailedAcquire),如果前置节点是SIGNAL状态,就挂起,返回true。如果前置节点状态为CANCELLED,就一直往前找,直到找到最近的一个处于正常等待状态的节点,并排在它后面,返回false,acquireQueed()接着自旋尝试,回到(3)。 -
(6) 前置节点处于其他状态,利用
CAS将前置节点状态置为SIGNAL。当前置节点刚释放资源,状态就不是SIGNAL了,导致失败,返回false。但凡返回false,就导致acquireQueed()接着自旋尝试。 -
(7) 最终当
tryAcquire(int)返回false,acquireQueued(Node,int)返回true,调用selfInterrupt(),中断当前线程
我们首先分析tryAcquire方法,接着分析addWaiter方法,acquireQueued方法,最后分析selfInterrupt方法。(分析过程中的主要被方法调用也会介绍)
tryAcquire方法
在「AQS底层设计--模板方法模式」中我们提到AQS使用了模板方法模式。
tryAcuire方法就是一个钩子方法。
在AQS中,此方法会抛出UnsupportedOperationException,所以需要子类去实现。(具体的子类实现我在博客ReentrantLock及源码分析有分析)
tryAcquire(arg)返回false,其实就是获取锁失败的情况。这时候就需要做自旋,重新获取。
addWaiter方法
// 添加等待者
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) { // 尾结点不为空,即已经被初始化
// 将node结点的prev域连接到尾结点
node.prev = pred;
if (compareAndSetTail(pred, node)) { // 比较pred是否为尾结点,是则将尾结点设置为node
// 设置尾结点的next域为node
pred.next = node;
return node; // 返回新生成的结点
}
}
enq(node); // 尾结点为空(即还没有被初始化过),或者是compareAndSetTail操作失败,则入队列
return node;
}
addWaiter方法使用快速添加的方式往sync queue尾部添加结点(将当前线程插入至队尾,返回在等待队列中的节点)
如果sync queue队列还没有初始化,则会使用enq插入队列中
接下来我们来分析enq方法。
-
enq方法private Node enq(final Node node) { for (;;) { // 无限循环,确保结点能够成功入队列 // 保存尾结点 Node t = tail; if (t == null) { // 尾结点为空,即还没被初始化 if (compareAndSetHead(new Node())) // 头节点为空,并设置头节点为新生成的结点 tail = head; // 头节点与尾结点都指向同一个新生结点 } else { // 尾结点不为空,即已经被初始化过 // 将node结点的prev域连接到尾结点 node.prev = t; if (compareAndSetTail(t, node)) { // 比较结点t是否为尾结点,若是则将尾结点设置为node // 设置尾结点的next域为node t.next = node; return t; // 返回尾结点 } } } }enq方法会使用无限循环来确保节点的成功插入。(将节点插入队尾,失败则自旋直到成功)
接着我们分析acquireQueue方法。
acquireQueue方法
// sync队列中的结点在独占且忽略中断的模式下获取(资源)
final boolean acquireQueued(final Node node, int arg) {
// 标志
boolean failed = true;
try {
// 中断标志
boolean interrupted = false;
for (;;) { // 无限循环
// 获取node节点的前驱结点
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { // 前驱为头节点并且成功获得锁
setHead(node); // 设置头节点
p.next = null; // help GC
failed = false; // 设置标志
return interrupted;
}
// 首先判断当前节点是否应该被park(挂起)
if (shouldParkAfterFailedAcquire(p, node) &&
// 确定有必要park才会执行该方法
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
首先获取当前节点的前驱节点,如果前驱节点是头节点并且能够获取(资源),代表该当前节点能够占有锁,设置头节点为当前节点,返回。否则,调用shouldParkAfterFailedAcquire和parkAndCheckInterrupt方法。
(自旋方式获取资源并判断是否需要被挂起)
接下来,我们先看shouldParkAfterFailedAcquire方法
-
shouldParkAfterFailedAcquire方法// 当获取(资源)失败后,检查并且更新结点状态 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { // 获取前驱结点的状态 int ws = pred.waitStatus; if (ws == Node.SIGNAL) // 状态为SIGNAL,为-1,说明前驱节点释放资源后会通知自己 // 可以进行安全的park操作,所以返回true return true; if (ws > 0) { // 表示状态为CANCELLED,为1,说明前驱节点已经放弃获取资源了 // 一直往前寻找 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); // 找到pred结点前面最近的一个状态不为CANCELLED的结点 // 赋值pred结点的next域 pred.next = node; } else { // 为PROPAGATE -3 或者是0 表示无状态,(为CONDITION -2时,表示此节点在condition queue中) // 比较并设置前驱结点的状态为SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } // 不能进行park操作 return false; }只有当该节点的前驱结点的状态为
SIGNAL时,才可以对该结点所封装的线程进行park操作。否则,将不能进行park操作。再看
parkAndCheckInterrupt方法。(判断当前节点是否应该被挂起,涉及到「内部类Node」中的等待状态)
-
parkAndCheckInterrupt方法// 进行park操作并且返回该线程是否被中断 private final boolean parkAndCheckInterrupt() { // 在许可可用之前禁用当前线程,并且设置了blocker LockSupport.park(this); return Thread.interrupted(); // 当前线程是否已被中断,并清除中断标记位 }parkAndCheckInterrupt方法里的逻辑是首先执行park操作,即禁用当前线程,然后返回该线程是否已经被中断。(确定有必要
park,才会执行该方法)再看
final块中的cancelAcquire方法。 -
cancelAcquire方法// 取消继续获取(资源) private void cancelAcquire(Node node) { // node为空,返回 if (node == null) return; // 设置node结点的thread为空 node.thread = null; // 保存node的前驱结点 Node pred = node.prev; while (pred.waitStatus > 0) // 找到node前驱结点中第一个状态小于0的结点,即不为CANCELLED状态的结点 node.prev = pred = pred.prev; // 获取pred结点的下一个结点 Node predNext = pred.next; // 设置node结点的状态为CANCELLED node.waitStatus = Node.CANCELLED; if (node == tail && compareAndSetTail(node, pred)) { // node结点为尾结点,则设置尾结点为pred结点 // 比较并设置pred结点的next节点为null compareAndSetNext(pred, predNext, null); } else { // node结点不为尾结点,或者比较设置不成功 // If successor needs signal, try to set pred's next-link // so it will get one. Otherwise wake it up to propagate. int ws; if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { // (pred结点不为头节点,并且pred结点的状态为SIGNAL)或者 // pred结点状态小于等于0,并且比较并设置等待状态为SIGNAL成功,并且pred结点所封装的线程不为空 // 保存结点的后继 Node next = node.next; if (next != null && next.waitStatus <= 0) // 后继不为空并且后继的状态小于等于0 compareAndSetNext(pred, predNext, next); // 比较并设置pred.next = next; } else { unparkSuccessor(node); // 释放node的前一个结点 } node.next = node; // help GC } }该方法完成的功能就是取消当前线程对资源的获取,即设置该结点的状态为
CANCELLED。接着我们再看
unparkSuccessor方法。-
unparkSuccessor方法// 释放后继结点 private void unparkSuccessor(Node node) { // 获取node结点的等待状态 int ws = node.waitStatus; if (ws < 0) // 状态值小于0,为SIGNAL -1 或 CONDITION -2 或 PROPAGATE -3 // 比较并且设置结点等待状态,设置为0 compareAndSetWaitStatus(node, ws, 0); // 获取node节点的下一个结点 Node s = node.next; if (s == null || s.waitStatus > 0) { // 下一个结点为空或者下一个节点的等待状态大于0,即为CANCELLED // s赋值为空 s = null; // 从尾结点开始从后往前开始遍历 for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) // 找到等待状态小于等于0的结点,找到最前的状态小于等于0的结点 // 保存结点 s = t; } if (s != null) // 该结点不为为空,释放许可 LockSupport.unpark(s.thread); }该方法的作用就是为了释放node节点的后继结点。
对于
cancelAcquire与unparkSuccessor方法,如下示意图可以清晰的表示:其中node为参数,在执行完
cancelAcquire方法后的效果就是unpark了s结点所包含的t4线程。现在,再来看
acquireQueued方法的整个的逻辑。逻辑如下:- 判断结点的前驱是否为head并且是否成功获取(资源)。
- 若步骤1均满足,则设置结点为head,之后会判断是否finally模块,然后返回。
- 若步骤2不满足,则判断是否需要
park当前线程,是否需要park当前线程的逻辑是判断结点的前驱结点的状态是否为SIGNAL,若是,则park当前结点,否则,不进行park操作。 - 若
park了当前线程,之后某个线程对本线程unpark后,并且本线程也获得机会运行。那么,将会继续进行步骤①的判断。
-
最后我们分析selfInterrupt方法
selfInterrupt方法
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
对当前线程产生一个中断请求。
能走到这个方法,说明acquireQueued()返回true,就进行自我中断。
acquiredShared方法
acquiredShared方法--共享模式获取资源
public final void acquireShared(int arg) {
//模板方法模式,tryAcquireShared由子类实现
//想看的话推荐读写锁的源码,这里就不细述了
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
以共享模式获取对象,忽略中断。先是tryAcquireShared(int)尝试直接去获取资源,如果成功,acquireShared(int)就结束了;否则,调用doAcquireShared(Node)将线程加入等待队列,直到获取到资源为止。
我们接着分析doAcquireShared方法
doAcquireShared方法
private void doAcquireShared(int arg) {
//将线程加入等待队列,设置为共享模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
//自旋尝试获取资源
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
//设置头节点,且如果还有剩余资源,唤醒后继节点获取资源
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
//是否需要被挂起
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
实现上和acquire()方法差不多,就是多判断了是否还有剩余资源,唤醒后继节点。
接着我们补充一个方法:hasQueuedPredecessors
hasQueuedPredecessors方法
ReentrantLock如果是公平锁的话,会在自己的tryAcquire的实现中调用AQS中的这个方法
public final boolean hasQueuedPredecessors() {
//判断当前节点在等待队列中是否为头节点的后继节点(头节点不存储数据),
//如果不是,则说明有线程比当前线程更早的请求资源,
//根据公平性,当前线程请求资源失败。
//如果当前节点没有前驱节点的话,才有做后面的逻辑判断的必要性
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
判断当前线程是否位于CLH同步队列中的第一个。如果是则返回flase,否则返回true。
release方法
release--独占方式释放资源
以独占模式释放对象,其源码如下:
public final boolean release(int arg) {
if (tryRelease(arg)) { // 释放成功
// 保存头节点
Node h = head;
if (h != null && h.waitStatus != 0) // 头节点不为空并且头节点状态不为0
unparkSuccessor(h); //释放头节点的后继结点
return true;
}
return false;
}
其中,tryRelease的默认实现是抛出异常,需要具体的子类实现,如果tryRelease成功,那么如果头节点不为空并且头节点的状态不为0,则释放头节点的后继结点。
我们接着分析unparkSuccessor方法
-
unparkSuccessor方法private void unparkSuccessor(Node node) { //如果状态为负说明是除CANCEL以外的状态, //尝试在等待信号时清除。 int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); Node s = node.next; //下一个节点为空或CANCELLED状态 //则从队尾往前找,找到正常状态的节点作为之后的继承人 //也就是下一个能拿到资源的节点 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); }尝试找到下一位继承人,就是确定下一个获取资源的线程,唤醒指定节点的后继节点。
releaseShared方法
releaseShared()--共享模式释放资源
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
在释放一部分资源后就可以通知其他线程获取资源。
接着我们分析doReleaseShared方法
doReleaseShared方法
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
//将自旋尝试修改等待状态为0
continue;
//唤醒下一个被阻塞的节点
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
//设置为PROPAGATE状态
continue;
}
//上面代码块执行过程中如果head变更了,就接着循环尝试
if (h == head)
break;
}
}
把当前结点的状态由SIGNAL设置为PROPAGATE状态。
AQS组件
AQS有哪些组件?都是拿来干什么的呢?
-
Semaphore(信号量)-允许多个线程同时访问:synchronized和ReentrantLock都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。Semaphore在限制资源访问量的问题上用处很大,比如限制一个文件的并发访问次数等。 -
CountDownLatch(倒计时器):CountDownLatch是一个同步工具类,用来协调多个线程之间的同步。这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。 -
CyclicBarrier(循环栅栏):CyclicBarrier和CountDownLatch非常类似,它也可以实现线程间的技术等待,但是它的功能比CountDownLatch更加复杂和强大。主要应用场景和CountDownLatch类似。CyclicBarrier的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier默认的构造方法是CyclicBarrier(int parties),其参数表示屏障拦截的线程数量,每个线程调用await()方法告诉CyclicBarrier我已经到达了屏障,然后当前线程被阻塞。
小结
本篇我们主要分析了AQS源码,详细的拆解成内部类,类的属性,还有几个主要方法。
AQS是我们JUC的核心,我们在最后也介绍了JUC中的几个常用同步器和它们的用法,当然详细内容及源码解释得另开章节分析了。
本篇参考: