队列同步器 AQS
一、概述
队列同步器 AbstractQueuedSynchronizer(简称AQS)
,是用来构建锁或者其他同步组件的基础框架。
AQS 依托于一个 volatile
修饰的变量 state
进行状态管理,AQS 内部提供了如下三个方法进行状态更新,它们能保证状态的改变是安全的。
getState()
setState(int newState)
compareAndSetState(int expect, int update)
一般推荐将同步器(继承 AQS 实现的子类)定义为同步组件(锁)的静态内部类(具体可参考ReentrantLock
),AQS 自身没有实现任何同步接口,它仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器支持两种方式获取同步状态:
- 互斥模式(exclusive): 支持独占式地获取同步状态;
- 共享模式(shared): 支持共享式地获取同步状态;
说明:
独占式(独占锁): 在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁。
共享式: 在同一时刻能有多个线程同时获取到同步状态(例如读写锁ReentrantReadWriteLock
里的读锁)。
常见的同步组件:
ReentrantLock
ReentrantReadWriteLock
CountDownLatch
等
二、同步器的方法
1. 同步器可重写的方法
自定义同步器 (AQS) 时,可以重写如下几个方法:
独占式:
tryAcquire(int)
:独占方式。获取资源成功,返回true;反之,返回false。
tryRelease(int)
:独占方式。释放资源成功,返回true;反之,返回false。
共享式:
tryAcquireShared(int)
:共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int)
:共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
isHeldExclusively()
:判断该线程是否正在独占资源。只有用到Condition时,才需要去实现它。
2. 同步器中的模板方法
自定义同步组件时,将使用同步器提供的模板方法来实现。
同步器提供的模板方法基本分为3类:
- 独占式获取与释放同步状态;
- 共享式获取与释放同步状态;
- 查询同步队列中的等待线程情况。
三、独占式
独占式获取同步状态:acquire(int arg)
独占式释放同步状态:release(int arg)
1. 获取同步状态
acquire(int arg)
以独占方式获取资源,且忽略线程中断的影响。如果获取到资源,线程直接返回;否则进入同步队列,直到获取到资源为止。
涉及以下4个步骤:
tryAcquire()
:先尝试直接去获取资源,如果成功则返回 true;addWaiter()
:如果步骤1返回false
,证明当前线程没有获取到资源,则将该线程加入同步队列的尾部,并标记为独占模式;acquireQueued()
:使步骤2的线程进入自旋状态(循环获取资源),获取资源成功后返回(退出自旋状态)。在整个自旋过程中,如果线程被中断过,则返回true,否则返回false。- 在步骤3过程中,即使线程中断也不被响应,所以在步骤4当中需要处理步骤3是否中断的操作。如果步骤3中线程中断,则在步骤4要将线程中断的操作补上,因此调用了
selfInterrupt()
方法。
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*/
public final void acquire(int arg) {
// 尝试获取资源,如果获取到,则tryAcquire()返回true
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 当acquireQueued()过程中发生了线程中断,这里需要调用中断当前线程的方法。
selfInterrupt();
}
static void selfInterrupt() {
// 中断当前线程
Thread.currentThread().interrupt();
}
tryAcquire(int arg)
尝试以独占的方式获取资源,如果获取成功,则直接返回true,否则直接返回false。
该方法默认需要子类实现。
/**
* Attempts to acquire in exclusive mode. This method should query
* if the state of the object permits it to be acquired in the
* exclusive mode, and if so to acquire it.
*/
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
addWaiter(Node mode)
Node的模式有两种:
Node.EXCLUSIVE (互斥模式)
、Node.SHARED (共享模式)
该方法将当前线程和资源占有模式包装成一个Node节点,并将Node节点加入到同步队列的队尾,最后返回当前线程所在的结点。
- 如果队列不为空,则以通过 CAS 的方式将当前线程节点加入到同步队列的末尾。
- 如果队列为空,则通过
enq(node)
方法初始化一个同步队列,并返回当前节点。
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 = pred;
// 同步队列不为空,则通过CAS方法将当前节点安全的添加到同步队列中;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 执行到这里说明同步队列为空
enq(node);
return node;
}
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
private Node enq(final Node node) {
// 使当前线程进入自旋(避免了线程的上下文切换开销,但自旋)
for (;;) {
Node t = tail;
// 如果同步队列尾节点为null,说明同步队列为空
if (t == null) { // Must initialize
// 通过CAS方法给同步队列添加一个头部(Head)Node节点,作为当前获取到资源线程的节点(这个节点是个空节点,没有线程信息)
if (compareAndSetHead(new Node()))
// 将创建的head节点赋值给tail节点(判断同步队列是否为空是根据tail==null来判断的)
tail = head; //执行完这个步骤后,进入下一个循环,因此会走到else逻辑
} else {
// 此处的t其实就是head节点,这里将需要添加的节点的prev跟同步队列进行了关联;
node.prev = t;
if (compareAndSetTail(t, node)) {
// 这里将同步队列尾节点的next跟需要添加的节点进行了关联;
t.next = node; // 到了这里就完成了双向队列的添加操作
return t;
}
}
}
}
acquireQueued(final Node node, int arg)
调用该方法使队列中的线程进行自旋(循环获取同步状态),直到拿到锁之后再返回。
主要逻辑分两部分:
- 逻辑1:如果当前节点的前驱节点(prev)是head结点,尝试获取锁(tryAcquire)成功,然后返回;
- 逻辑2:否则检查当前节点是否应该被park,然后将该线程park并且检查当前线程是否被可以被中断;
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*/
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
// 获取当前加入节点的前驱节点
final Node p = node.predecessor();
/*
* 逻辑1:判断前驱节点是否是head节点
* (如果是head节点,说明当前节点(node)是同步队列中排在第一个,可以尝试获取同步资源)
*/
if (p == head && tryAcquire(arg)) {
/*
* 执行到这里说明,head节点关联的线程以及释放资源,且node节点获取到资源,
* 所以需要将当前节点设置为head节点(从逻辑上可以理解为head节点关联的线程是获取同步资源的线程)。
*/
setHead(node);
// 这里将老的head节点的next设置为null,next原先的引用指向加入进来的node节点。
p.next = null; // help GC
failed = false;
return interrupted;
}
// 逻辑2:检查当前节点是否应该被park,然后将该线程park并且检查当前线程是否被可以被中断
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true; //线程被标记为中断
}
} finally {
if (failed)
cancelAcquire(node);
}
}
思考:
问: 为什么只有当前节点的前继节点是 head节点时,才尝试去获取同步状态呢?
答: 实现获取同步状态的公平性。在ReentrantLock中公平锁的实现,就是依赖这个实现的。
问: ReentrantLock中非公平锁又是怎么实现的呢?
答: 在获取锁时,让当前线程直接参与同步状态的竞争,而非在同步队列排队等待。
shouldParkAfterFailedAcquire(Node pred, Node node)
该方法通过对当前节点的前驱节点(prev)状态进行判断,对当前节点做出不同的操作:
- 前驱节点状态为
SIGNAL
:返回true,表示要阻塞当前节点的线程;- 前驱节点状态为
CANCELLED
:返回false。将该前驱节点从同步队列中删除,并继续往前找状态为非CANCELLED的前驱节点,循环此过程;- 当 waitstatus状态是 0 或者 PROPAGATE 时,把当前节点的状态设置为SIGNAL;
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
// 前驱节点还在等待触发,所以当前节点可以被park(还没轮到当前节点)
return true;
if (ws > 0) {//ws>0的只有CANCELLED状态
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
// 前驱节点状态如果是CANCELLED,则从同步队列中删除,并继续往前找状态为非CANCELLED的前驱节点,循环此过程;
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
// 执行到这里,waitstatus只可能有2种状态(0 或者 PROPAGATE),无论是哪个都需要把当前节点的状态设置为SIGNAL
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt()
该方法让线程去休息,真正进入等待状态。
private final boolean parkAndCheckInterrupt() {
// 让当前线程进入waiting状态
LockSupport.park(this); //通过unpark()和interrupt()可以唤醒该线程;
return Thread.interrupted();
}
小结:
获取独占式同步状态流程如下:
- 调用自定义同步器的
tryAcquire()
先尝试直接去获取资源,如果成功则直接返回;- 步骤1没有成功,证明当前线程没有获取到资源,则调用
addWaiter()
将该线程加入同步队列的尾部,并标记为独占模式;- 调用
acquireQueued()
方法,使步骤2的线程进入自旋状态(循环获取资源),获取资源成功后返回(退出自旋状态)。在整个自旋过程中,如果线程被中断过,则返回true,否则返回false。- 在步骤3过程中,即使线程中断也不被响应,所以在步骤4当中需要处理步骤3是否中断的操作。如果步骤3中线程中断,则在步骤4要将线程中断的操作补上,因此调用了
selfInterrupt()
方法。
流程图如下:
2. 释放同步状态
/**
* Releases in exclusive mode. Implemented by unblocking one or
* more threads if {@link #tryRelease} returns true.
* This method can be used to implement method {@link Lock#unlock}.
*/
public final boolean release(int arg) {
// 子类实现释放锁的策略
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 此处尝试释放节点
unparkSuccessor(h);
return true;
}
return false;
}
// 子类实现
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
unparkSuccessor(Node node)
该方法用于唤醒同步队列中下一个线程。
下一个线程: 指最靠近同步队列头部,且节点等待状态为非 CANCELLED
的节点所在的线程。
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
// 获取当前node节点的下一个节点(当前节点释放资源后,准备唤醒下个节点获取资源)
Node s = node.next;
// 满足下个节点为null 或者等待状态为CANCELLED
if (s == null || s.waitStatus > 0) {
s = null;
// 从同步队列尾部开始往头部遍历,获取到最靠近头部的,且状态为非CANCELLED的节点;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 唤醒线程
LockSupport.unpark(s.thread);
}
四、共享式
共享式获取同步状态:acquireShared(int arg)
共享式释放同步状态:releaseShared(int arg)
1. 获取同步状态
共享模式下获取同步状态:
- 先调用
tryAcquireShared(int arg)
方法尝试获取同步状态 (当返回值大于等于0时,表示能够获取到同步状态); - 步骤1没有获取到同步状态时,执行
doAcquireShared(arg)
进入共享式获取的自旋过程;如何退出自旋的过程?
在自旋过程中,会循环调用tryAcquireShared(int arg)
方法获取同步状态,成功获取到同步状态就退出自旋;
public final void acquireShared(int arg) {
// 返回值小于0,表示没有获取到同步状态
if (tryAcquireShared(arg) < 0)
// 共享式获取:进入自旋过程
doAcquireShared(arg);
}
// 返回值大于等于0时,表示能够获取到同步状态。
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
private void doAcquireShared(int arg) {
// 1.给同步队列添加一个Node.SHARED模式的节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {//自旋
// 获取当前节点的前驱节点
final Node p = node.predecessor();
//如果前驱节点是head结点,说明当前节点是head节点的后继节点
if (p == head) {
// 重新尝试获取同步状态
int r = tryAcquireShared(arg);
if (r >= 0) {//大于0表示获取到同步资源
// 将head节点替换为当前节点
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);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
// 如果还有剩余量,继续唤醒下一个相邻线程
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
2. 释放同步状态
public final boolean releaseShared(int arg) {
// 尝试是否锁,由子类实现
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// 尝试是否锁,由子类实现
protected boolean tryReleaseShared(int arg) {
throw new UnsupportedOperationException();
}
// 此方法主要用于唤醒后继
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h); //唤醒后继
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // head发生变化
break;
}
}
五、结语
到这里,我们分析完了独占式同步状态的获取与释放,共享式同步状态的获取与释放。
参考:《Java并发编程的艺术》
图片来源:《Java并发编程的艺术》