AbstractQueuedSynchronizer源码解析
1.释放锁
释放锁是在显式调用Lock.unlock()
方法时触发的,目的是让线程释放对资源的访问权。unlock方法的基础方法是release
和releaseShared
,分别代表排它锁和共享锁的释放。
释放排它锁—release方法
排它锁的释放过程比较简单,分为两步
- 首先尝试
tryRelease
方法释放锁,如果失败会返回 false;反之,如果成功则执行步骤2 - 判断同步队列中是否还有节点在等待,如果有,此时当前节点一定是同步队列的头节点。调用
unparkedSuccessor
方法唤醒同步队列中的一个后继节点
具体源码如下,
public final boolean release(int arg) {
// tryRelease 交给实现类去实现,一般就是用当前同步器状态减去arg
// 如果返回 true 说明成功释放锁。
if (tryRelease(arg)) {
Node h = head;
// 说明见下方
if (h != null && h.waitStatus != 0)
// 从同步队列头开始唤醒等待锁的节点
unparkSuccessor(h);
return true;
}
return false;
}
对上面的判断if (h != null && h.waitStatus != 0)
进行说明,
3. 头节点为空说明同步队列尚未初始化
4. 若头节点的节点状态为0,说明是在enq
方法中compareAndSetHead(new Node())
初始化的头节点。这种情况可能是其他节点加入同步队列后,由于一些原因取消了,被从同步队列中清除,导致队列中只有一个waitStatus=0
的节点
大部分情况下,当节点指向的线程释放锁时,该节点应该是同步队列的头节点。但是也有特殊情况,这是由于acquire
方法中的操作造成的。上面两种情况都说明,同步队列是空的。这种情况下,无需进行后续节点的唤醒操作。
unparkSuccessor方法
unparkSuccessor
方法是的作用是唤醒同步队列中下一个节点,
private void unparkSuccessor(Node node) {
// node节点是当前释放锁的节点,也是同步队列的头节点
int ws = node.waitStatus;
// 节点未被取消了,把节点的状态置为初始化
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 拿出 node 节点的后面一个节点
Node s = node.next;
// 判断说明见下方
if (s == null || s.waitStatus > 0) {
s = null;
// 从同步队列尾开始迭代,具体原因见下方
for (Node t = tail; t != null && t != node; t = t.prev)
// t.waitStatus <= 0 说明 t 没有被取消,肯定还在等待被唤醒
if (t.waitStatus <= 0)
s = t;
}
// 唤醒最靠近同步队列队头的状态不为CANCELLED的节点
if (s != null)
LockSupport.unpark(s.thread);
}
上方代码中的判断if (s == null || s.waitStatus > 0)
,
- s 为空,表示 node 的后一个节点为空
- s.waitStatus 大于0,代表 s 节点已经被取消了
遇到以上这两种情况,就从队尾开始,向前遍历,找到最靠近队头的一个 waitStatus
字段不是被取消的节点对象。
寻找最靠近同步队列头部的状态不为cancelled
的节点的过程是从同步队列尾部开始的,具体原因如下,
- 主要是因为节点被阻塞的时候,是在
acquireQueued
方法的for循环中被阻塞的,唤醒时也一定会在acquireQueued
方法的for循环里面被唤醒。 - 唤醒之后会进入新一轮循环,在循环中会判断当前节点的前置节点是否是头节点。
- 从尾到头的迭代顺序目的就是为了过滤掉无效的前置节点,不然节点被唤醒时,发现其前置节点还是无效节点,就又会陷入阻塞。
acquireQueued
方法过程说明见博文。
获取头节点的后继节点,当后继节点的时候会调用LookSupport.unpark()方法
,该方法会唤醒该节点的后继节点所包装的线程。因此,每一次锁释放后就会唤醒队列中该节点的后继节点所引用的线程,从而进一步可以佐证获得锁的过程是一个FIFO(先进先出)的过程。
共享锁释放—releaseShared方法
释放共享锁分为两步,
- 首先尝试
tryReleaseShared
释放当前共享锁,失败会返回 false,如果成功释放,则执行步骤2 - 调用
doReleaseShared
方法唤醒后继节点
源码如下,
// 共享模式下,释放当前线程的共享锁
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
// 这个方法就是线程在获得锁时,唤醒后续节点时调用的方法
doReleaseShared();
return true;
}
return false;
}
doReleaseShared方法
该方法跟独占式锁释放过程有点点不同,在共享式锁的释放过程中,对于能够支持多个线程同时访问的并发组件,必须保证多个线程能够安全的释放同步状态,这里采用的CAS保证,当CAS操作失败continue,在下一次循环中进行重试。
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))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
2. 条件队列重要方法
首先展示条件队列的结构,单向链表,
之前synchronized
代码中,利用Object的方式实际上是指在对象Object对象监视器上只能拥有一个同步队列和一个等待队列。而并发包中的Lock拥有一个同步队列和多个条件队列。示意图如
ConditionObject是AQS的内部类,因此每个ConditionObject能够访问到AQS提供的方法,相当于每个Condition都拥有所属同步器的引用。
进入条件队列—await方法
当调用condition.await()
方法后会使得当前获取lock的线程进入到条件队列,如果该线程能够从await()方法返回的话一定是该线程获取了与condition相关联的lock。
await方法源码如下,
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 加入到条件队列的队尾
Node node = addConditionWaiter();
// 完全释放所占用资源,说明见下方
int savedState = fullyRelease(node);
int interruptMode = 0;
// 确认node不在同步队列上,再阻塞,如果 node 在同步队列上,是不能够上锁的
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)
// 如果状态不是CONDITION,就会自动删除
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
await方法的具体流程,
- 调用
addConditionWaiter
方法将当前线程包装成Node,插入到条件队列的队尾 - 释放当前线程所占用的lock,在释放的过程中会唤醒同步队列中的下一个节点
- 进入 while循环,判断当前线程的节点不在同步队列中,之后阻塞。阻塞是在 while循环中发生的,所以唤醒后也是在while循环中。
- 当节点被
signal
或signalAll
方法从条件队列中调回同步队列后会跳出while循环并调用acquireQueued
方法获取lock
1)addConditionWaiter方法
该方法将线程包装为节点插入到条件队列的队尾,源码如下,
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果尾部的 waiter 不是 CONDITION 状态了,删除
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 新建条件队列 node
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 队列是空的,直接放到队列头
if (t == null)
firstWaiter = node;
// 队列不为空,直接到队列尾部
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
该方法返回的是包装后的Node对象。
该方法中,在将节点接到队尾之前会先判断条件队列尾节点的状态,如果不是Node.CONDITION
就会调用unlinkCancelledWaiters
方法删除条件队列中所有状态不为Node.CONDITION
的节点。
同时可以看出条件队列是一个不带头结点的链式队列,之前学习AQS时知道同步队列是一个带头结点的链式队列,这是两者的一个区别。
2)unlinkCancelledWaiters方法
该方法从条件队列头部开始,删除掉所有状态不对的节点,源码如下,
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
// trail 表示上一个状态,这个字段作用非常大,可以把状态都是 CONDITION 的 node 串联起来,即使 node 之间有其他节点都可以
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
// 当前node的状态不是CONDITION,删除自己
if (t.waitStatus != Node.CONDITION) {
//删除当前node
t.nextWaiter = null;
// 如果 trail 是空的,循环又是从头开始的,说明从头到当前节点的状态都不是 CONDITION
// 都已经被删除了,所以移动队列头节点到当前节点的下一个节点
if (trail == null)
firstWaiter = next;
// 如果找到上次状态是CONDITION的节点的话,先把当前节点删掉,然后把自己挂到上一个状态是 CONDITION 的节点上
else
trail.nextWaiter = next;
// 遍历结束,最后一次找到的CONDITION节点就是尾节点
if (next == null)
lastWaiter = trail;
}
// 状态是 CONDITION 的 Node
else
trail = t;
// 继续循环,循环顺序从头到尾
t = next;
}
}
流程示意图如下,
3)fullyRelease方法
当前节点插入到等待对列之后,会使当前线程释放lock,由fullyRelease
方法实现,源码如下,
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
if (release(savedState)) {
//成功释放同步状态
failed = false;
return savedState;
} else {
//不成功释放同步状态抛出异常
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
调用AQS的方法release
方法释放AQS的同步状态并且唤醒在同步队列中头结点的后继节点引用的线程,如果释放成功则正常返回,若失败的话就抛出异常。
4)从await方法中退出
while (!isOnSyncQueue(node))
退出循环的条件是节点从条件队列中取出放回至同步队列。目前想到的只有两种可能,
- node 刚被加入到条件队列中,立马就被其他线程 signal 转移到同步队列中去了
- 线程之前在条件队列中沉睡,被唤醒后加入到同步队列中去
退出循环后直接尝试 acquireQueued
方法在同步队列中阻塞直至获取锁。
线程唤醒
1)单个线程唤醒—signal方法
调用condition的signal或者signalAll方法可以将条件队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得lock。按照条件队列是先进先出(FIFO)的,所以条件队列的头节点必然会是等待时间最长的节点,也就是每次调用condition的signal方法是将头节点移动到同步队列中。
该方法对应下图的浅蓝色曲线的过程,
signal
方法是ConditionObject类中的方法,源码如下,
public final void signal() {
// 第一步检验
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 从头节点开始唤醒
Node first = firstWaiter;
if (first != null)
// doSignal 方法会把条件队列中的节点转移到同步队列中去
doSignal(first);
}
signal
方法首先会检测调用signal
方法的线程是否已经获取lock,如果没有获取lock会直接抛出异常,如果获取的话再得到等待队列的头指针引用的节点- 之后的操作的
doSignal
方法也是基于该节点。
调用ConditionObject对象的signal
方法的前提条件是当前线程已经获取了lock,该方法会使得条件队列中的头节点,即等待时间最长的那个节点移入到同步队列。移入到同步队列后才有机会使得等待线程被唤醒,即从await方法中的LockSupport.park(this)
方法中返回,从而才有机会使得调用await方法的线程成功退出。
条件队列头节点移入同步队列—doSignal方法
该方法源码如下,
private void doSignal(Node first) {
do {
// firstWaiter指向当前节点的nextWaiter
// 若firstWaiter为空,说明到队尾了
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 从队列头部开始唤醒
first.nextWaiter = null;
// transferForSignal 方法会把节点转移到同步队列中去
// (first = firstWaiter) = null 说明队列中的元素已经循环完了
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
doSignal
方法将条件队列头节点的后置节点置为 null,这种操作其实就是把 node 从条件队列中移除。- 通过 while 保证
transferForSignal
方法能执行成功
唤醒过程最关键方法—transferForSignal
transferForSignal
方法会把节点转移到同步队列中去,该方法是真正对头节点做处理的逻辑,源码如下,
// 传入参数 node 是条件队列的头节点
final boolean transferForSignal(Node node) {
// 将 node 的状态从 CONDITION 修改成初始化,失败返回 false
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 当前队列加入到同步队列,返回的 p 是 node 加入同步队列后的前置节点
Node p = enq(node);
int ws = p.waitStatus;
// 状态修改成 SIGNAL,如果成功直接返回
// 把前置节点p的状态改成 SIGNAL 是因为 SIGNAL 本身就表示该节点后面的节点都是需要被唤醒的
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 如果 p 节点被取消,或者状态不能修改成SIGNAL,直接唤醒
LockSupport.unpark(node.thread);
return true;
}
transferForSignal
方法返回 true 表示转移成功, false 表示转移失败。
2)全部线程唤醒—signalAll方法
signalAll
方法的作用是唤醒条件队列中全部节点,源码如下,
public final void signalAll() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 拿到头节点
Node first = firstWaiter;
if (first != null)
// 从头节点开始唤醒条件队列中所有的节点
doSignalAll(first);
}
上方代码与signal
方法的区别仅在于将内部调用的方法从doSignal
改为doSignalAll
。
条件队列所有节点移入同步队列—doSignalAll方法
把条件队列所有节点依次转移到同步队列去,
// 传入参数 node 是条件队列的头节点
private void doSignalAll(Node first) {
lastWaiter = firstWaiter = null;
do {
// 拿出条件队列队列头节点的下一个节点
Node next = first.nextWaiter;
// 把头节点从条件队列中删除
first.nextWaiter = null;
// 头节点转移到同步队列中去
transferForSignal(first);
// 开始循环头节点的下一个节点
first = next;
} while (first != null);
}
该方法将条件队列中的每一个节点都移入到同步队列中,即“通知”当前调用ConditionObject#await()
方法的每一个线程。
3. await与signal/signalAll的结合思考
图示
使用 ConditionObject 提供的await
和signal/signalAll
方法就可以实现这种机制,而这种机制能够解决最经典的问题就是“生产者与消费者问题”。
await和signal和signalAll方法就像一个开关控制着线程A(等待方)和线程B(通知方)。它们之间的关系可以用下面一个图来表现,
- 线程awaitThread先通过
lock.lock()
方法获取锁成功后调用了condition.await
方法进入等待队列 - 另一个线程signalThread通过
lock.lock()
方法获取锁成功后调用了condition.signal
或者signalAll
方法,使得线程awaitThread能够有机会移入到同步队列中 - 当其他线程释放lock后使得线程awaitThread能够有机会获取lock,从而使得线程awaitThread能够从await方法中退出执行后续操作。如果awaitThread获取lock失败会直接进入到同步队列。
示例
public class AwaitSignal {
private static ReentrantLock lock = new ReentrantLock();
private static Condition condition = lock.newCondition();
private static volatile boolean flag = false;
public static void main(String[] args) {
Thread waiter = new Thread(new waiter());
waiter.start();
Thread signaler = new Thread(new signaler());
signaler.start();
}
static class waiter implements Runnable {
@Override
public void run() {
lock.lock();
try {
while (!flag) {
System.out.println(Thread.currentThread().getName() + "当前条件不满足等待");
try {
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "接收到通知条件满足");
} finally {
lock.unlock();
}
}
}
static class signaler implements Runnable {
@Override
public void run() {
lock.lock();
try {
flag = true;
condition.signalAll();
} finally {
lock.unlock();
}
}
}
}
上面代码的执行结果,
Thread-0当前条件不满足等待
Thread-0接收到通知,条件满足