AbstractQueuedSynchronizer源码解析
AbstractQueuedSynchronizer是同步器,简称AQS,是各类线程锁的基础。该类的方法很多,针对包装线程的节点的操作也很多。
1.整体架构
AQS整体处理流程如下,
对上图进行说明,
- AQS中的队列有两种,同步队列和条件队列,其底层数据结构是链表
- 四种颜色的线条代表不同的场景
AQS本身就是一套锁的框架,定义了获得锁和释放锁的代码,所以继承AQS抽象类并实现相应的方法即可实现锁。
类注释
AQS的类注释中包含的信息如下,
- AQS提供了一个框架,定义了先进先出的同步队列,让获取不到锁的线程在同步队列中排序
- 同步器存在一个成员变量
status
,表示同步器的状态,用于判断AQS是否能够得到锁。该变量使用volatile
关键字进行修饰保证其线程安全 - AQS的子类可以通过CAS的方式给
status
赋值,定义哪些状态可以获取锁,哪些状态获取不到锁 - AQS提供两种锁模式,共享锁和排它锁。
i) 排他模式:只有一个线程可以获得锁
ii)共享模式:多个线程可以同时获得锁
AQS的子类 ReadWriteLock实现了这两种模式 - AQS的内部类ConditionObject是条件队列的实现类。通过
new Condition()
可以创建一个条件队列。锁对象中,条件队列的数目可以是多个 - AQS继承了 AbstractOwnableSynchronizer类,该类可以追踪获得锁的线程
类定义
AQS的类定义如下,
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {}
- AQS是抽象类,该类中只实现类将线程放入和取出同步队列、条件队列的方法,定义了如何获得锁、如何释放锁的抽象方法,目的是让子类去实现争锁和释放锁的过程
- 继承了AbstractOwnableSynchronizer类,该类的作用就是为了知道哪个线程获取了锁,便于管理
类属性
1)一般属性
// 同步器状态,子类会根据状态字段进行判断是否可以获得锁或释放锁
// CAS给该变量赋值,获得锁+1,释放锁-1
private volatile int state;
// 自旋超时阀值,单位纳秒。当设置等待时间时才会用到这个属性
static final long spinForTimeoutThreshold = 1000L;
2)同步队列属性
同步队列的说明如下,
- 当多个线程都来请求锁时,某一时刻有且只有一个线程能够获得锁(排它锁)。其余获取不到锁的线程,都会到同步队列中去排队并阻塞自己。
- 当有线程主动释放锁时,就会从同步队列头开始释放一个排队的线程,让线程重新去竞争锁。
- 同步队列的底层是双向链表
private transient volatile Node head;
private transient volatile Node tail;
同步队列的头和尾,是AQS的属性。
3) 条件队列属性
条件队列的说明如下,
- 条件队列同样用于管理获取不到锁的线程
- 条件队列的底层是单向链表(与同步队列区分)
- 条件队列不直接和锁打交道
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
// 条件队列中第一个 node
private transient Node firstWaiter;
// 条件队列中最后一个 node
private transient Node lastWaiter;
}
条件队列实现的是 Condition接口。
4)Node
Node既是同步队列的节点,同时也是条件队列的节点。该类用于包装线程,
static final class Node {
// 同步队列单独的属性
//node共享模式或排他模式
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
// 当前节点的前节点
// 节点 acquire 成功后就会变成head
// head 节点不能被 cancelled
volatile Node prev;
// 当前节点的下一个节点
volatile Node next;
// 两个队列共享的属性
// 表示当前节点的状态,通过节点的状态来控制节点的行为
// 普通同步节点,就是 0 ,条件节点是 CONDITION -2
volatile int waitStatus;
// waitStatus 的状态有以下几种
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
// 当前节点包装的线程
volatile Thread thread;
// 在同步队列中,nextWaiter并不真的是指向其下一个节点,只是表示当前 Node 是排它模式还是共享模式
// 但在条件队列中,nextWaiter 就是表示下一个节点元素
Node nextWaiter;
}
Node类中的waitStatus
一定与AQS的status
进行区分,Node的操作是通过waitStatus
决定的。
对Node节点的状态进行如下说明,
状态 | 说明 |
---|---|
CANCELLED | 被取消 |
SIGNAL | 同步队列中的节点在自旋获取锁的时候, 如果前一个节点的状态是 SIGNAL,那么自己就可以阻塞休息了;否则自己一直自旋尝试获得锁 |
CONDITION | 表示当前 node 正在条件队列中。当节点从同步队列转移到条件队列时,状态就会被更改成 CONDITION |
PROPAGATE | 无条件传播,共享锁模式下,该状态的进程处于可运行状态 |
5)共享锁和排它锁的区别
同一时刻只能有一个线程可以获得排它锁,也只能有一个线程可以释放锁。
共享锁同一时刻允许多个线程获得同一个锁,并可以设置获取锁的线程的数目。
Condition接口
条件队列ConditionObject实现了Condition接口,
1)类注释
- 当使用锁对象替代 synchronize时,Condition用于替代 Object中的监控方法,如
Object#wait()
、Object#notify()
和Object#notifyAll()
等方法 - 线程被暂停执行后,等待其他线程将其唤醒
- Condition实例绑定在锁对象上,通过
Lock#newCondition()
方法可以创建锁的条件队列
2)条件队列示例
假设存在一个有边界的队列,支持 put和 take方法存入或取出元素,
- 如果试图向空队列执行
take
操作,线程将会阻塞,直到队列中有可用的元素为止 - 如果试图向满队列执行
put
操作,线程将会阻塞,直到队列中有空闲的位置为止
上面的例子中,如果两个操作依靠一个条件队列,那么每次只能执行其中一个操作。所以可以创建两个条件队列,分别执行存入和取出的操作。
3)接口方法
Condition接口定义了一些方法,这些方法奠定了条件队列的基础,
void await() throws InterruptedException;
该方法使当前线程一直等待,直到被signal
或signalAll
方法唤醒。
条件队列中的线程被唤醒的四种情况,
- 有线程使用了
signal
方法,唤醒了条件队列中的当前线程。该方法唤醒条件队列中的一个线程,在被唤醒前必须先获得锁 - 有线程使用了
signalAll
方法,该方法唤醒条件队列中的所有线程 - 其他线程打断了当前线程
- 虚假唤醒
线程从条件队列中苏醒时,必须重新获得锁,才能真正被唤醒。
2.同步器状态
AQS中存在两个状态,status
和waitStatus
,二者一定要区分,
- status是锁的状态,是 int 类型。子类继承 AQS 时,都是要根据 state 字段来判断有无得到锁,比如当前同步器状态是 0,表示可以获得锁,当前同步器状态是 1,表示锁已经被其他线程持有,当前线程无法获得锁;
- waitStatus 是节点(Node)的状态,种类很多,一共有初始化 (0)、CANCELLED (1)、SIGNAL (-1)、CONDITION (-2)、PROPAGATE (-3),各个状态的含义可以见上文。
3.获取锁
在AQS的子类中通常使用Lock#lock()
方法获得锁,使得线程能够取得资源的使用权限。Lock是AQS的子类,lock
方法或根据情况调用AQS的acquire
和tryAcquire
方法。
acquire
方法 AQS 已经实现了,tryAcquire
方法是等待子类去实现。
- acquire 方法制定了获取锁的框架,先尝试使用 tryAcquire 方法获取锁,获取不到时,再入同步队列中等待锁
- tryAcquire 方法在 AQS 中直接抛出一个异常,表明需要子类去实现,子类可以根据同步器的 state 状态来决定是否能够获得锁
获得排它锁—acquire
public final void acquire(int arg) {
// tryAcquire 方法是需要实现类去实现的,实现思路一般都是 cas 给 state 赋值来决定是否能获得锁
if (!tryAcquire(arg) &&
// addWaiter 入参代表是排他模式
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
该流程对应架构图的红线部分,
具体流程为,
- 尝试执行一次
tryAcquire
方法,如果成功则线程执行任务即可。如果失败则走下面的流程进入等待队列 - 调用
addWaiter
方法将线程包装成Node,添加到同步队列尾部。addWaiter
方法返回包装了当前线程的Node对象 - 调用
acquireQueued
方法,该方法的作用有两个,
i. 阻塞当前线程
ii. 节点被唤醒时,使其能够获得锁
这两个功能的实现是依靠acquireQueued
方法中自旋过程 acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
如果返回值为true
说明线程是在同步队列中阻塞一段时间后才被取出,此时线程的interrupt
状态为true,调用selfInterrupt()
将interrupt
改为false
1)addWaiter方法
用于将未获取到锁的线程添加到同步队列尾部,
private Node addWaiter(Node mode) {
// 初始化包装当前线程的Node对象
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
// 如果同步队列不为空,先简单尝试将节点加到队列尾部,通常情况下会成功加入
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 如果队列为空或简单尝试失败则自旋保证node加入到队尾
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 如果队尾为空,说明当前同步队列都没有初始化,进行初始化
// tail = head = new Node();
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else { // 队尾不为空,将当前节点追加到队尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
在 addWaiter 方法中,并没有进入方法后立马就自旋,而是先尝试一次追加到队尾,如果失败才自旋,因为大部分操作可能一次就会成功,这种思路在写自旋的时候可以借鉴。
2)acquireQueued方法
将线程加入到同步队列尾部后,需要使当前线程阻塞,acquireQueued
方法用于实现这一功能,
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
final Node p = node.predecessor(); // 获取当前线程节点的前一个节点
// 有两种情况会走到 p == head,见下方说明
if (p == head && tryAcquire(arg)) {
// 成功获得锁,则将自身设置成head节点并回收其前置节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// shouldParkAfterFailedAcquire和parkAndCheckInterrupt 用于阻塞当前线程
// 线程是在这个方法里面阻塞的,醒来的时候仍然在for循环里面,就能再次自旋尝试获得锁
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 如果获得node的锁失败,将 node 从队列中移除
if (failed)
cancelAcquire(node);
}
}
方法的自旋过程在每一次循环开始时都会有一步判断p == head
,满足这一条件通常有两种情况,
- node 之前没有获得锁,
addWaiter
方法中调用enq
方法初始化同步队列后将其添加到了新创建的head
节点之后。
此时进入 acquireQueued 方法时,才发现当前节点的前置节点就是头节点,于是尝试获得一次锁 - node节点之前一直在阻塞沉睡,然后被唤醒(唤醒操作从
head
节点开始)。此时唤醒 node 的节点正是其前一个节点。
如果 tryAcquire 成功,就立马把自己设置成 head,把其前置节点移除(因为前置节点是之前的head
,同步队列的头节点相当于一个没有用的空节点)。
如果 tryAcquire 失败,尝试进入同步队列。
对acquireQueued
方法的返回值进行说明,
- 返回
true
,说明节点是进入到同步队列之后阻塞了一段时间才被从队列中取出 - 返回
false
,说明节点进入了同步队列,但是没等到被阻塞就被从队列中取出,即没有执行到shouldParkAfterFailedAcquire
和parkAndCheckInterrupt
方法
setHead方法
将节点设置为同步队列的头节点,
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
同步队列的头节点相当于一个空节点,其作用只是指向其下一个节点。
shouldParkAfterFailedAcquire方法
该方法用于将当前的Node对象的前置节点waitStatus
设置为SIGNAL
,前置节点的状态为SIGNAL时,当前节点就可以阻塞。
// 参数是当前节点的前置节点和当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前置节点状态已经是SIGNALED,说明当前节点不是新入队的节点
// 该线程可以安全的被park阻塞
if (ws == Node.SIGNAL)
return true;
// 前置节点是CANCELLED状态说明该节点无效,将当前节点挂到更前面的有效节点之后
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前置节点状态是0或PROPAGATE说明当前节点是新入队的节点
// 需要将前置节点的状态改为SIGNAL,但是不立刻对其执行park操作。而是返回false,回到自旋过程中在新一轮循环中才阻塞
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
对shouldParkAfterFailedAcquire
方法返回值的说明,
- 返回 true,前置节点状态已经是SIGNALED,当前节点的线程可以被安全阻塞
- 返回 false,前置节点状态刚刚被调整为SIGNALED,当前节点的线程不立刻阻塞。而是回到
acquireQueued
方法的自旋过程中在新一轮循环中阻塞
parkAndCheckInterrupt方法
该方法用于阻塞线程,线程是在这个方法里面阻塞的,醒来的时候仍然在acquireQueued
方法的 for 循环里面,就能再次自旋尝试获得锁,
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
获取共享锁—acquireShared方法
acquireShared 整体流程和 acquire 相同,代码也很相似,贴出来不一样的代码进行比较,
1)尝试获取锁—tryAcquireShared方法
2)setHeadAndPropagate方法
第二步不同,在于节点获得排它锁时,仅仅把自己设置为同步队列的头节点即可(setHead
方法)。但如果是共享锁的话,还会去唤醒自己的后续节点,一起来获得该锁(setHeadAndPropagate
方法)。
不同之处如下(左边排它锁,右边共享锁),
setHeadAndPropagate方法源码如下,
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
// propagate > 0 表示已经有节点获得共享锁了
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//共享模式,还唤醒头节点的后置节点
if (s == null || s.isShared())
doReleaseShared();
}
}
// 释放后置共享节点
private void doReleaseShared() {
for (;;) {
Node h = head;
// 还没有到队尾,此时队列中至少有两个节点
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果队列状态是 SIGNAL ,说明后续节点都需要唤醒
if (ws == Node.SIGNAL) {
// CAS 保证只有一个节点可以运行唤醒的操作
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 进行唤醒操作
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
总结
本节的重点是获取排它锁的acquire
方法,整体的流程图如下,