二者区别
首先,AQS中的 同步队列 和 条件队列 是两种不同队列:
- 目的不同:同步队列主要用于实现锁机制(也就是锁的获取和释放),而条件队列用于实现条件变量,条件变量是并发编程中一种用于线程间通信的机制,它允许一个或多个线程在特定条件成立之前等待并释放相关的锁,直到其他线程改变了条件并显式的唤醒等待在该条件中的线程。比较典型的一个条件队列使用场景就是 ReentrantLock 的 Condition。
- 使用方式不同:同步队列是AQS自动管理的,开发者通常不需要直接与之交互;而条件队列是通过Condition接口暴露给开发者的,需要显示地调用 等待await()方法 和 通知signal()/signalAll()方法
- 队列类型不同:虽然它们都是队列结构,但同步队列是所有基于AQS同步器共享的,每个同步器实例只有一个同步队列;但条件队列是每个 Condition实例特有的,一个同步器可以有多个 Condition对象,因此也就有多个条件队列。
实现原理
同步队列
同步队列的实现原理比较简单,AQS中同步队列是一个FIFO队列,节点类型为AQS的内部类Node。Node有两个指针,prev和next,分别指向前置节点和后置节点,一个个Node节点组成了一个双向链表。
- 当一个线程尝试获取锁失败时,它会被封装成一个Node节点加入到队列尾部。
- 这个节点会处于等待状态,直到锁被其他线程释放。
- 当锁被释放时,队列的头节点(持有锁的线程)会通知其后继节点(如果存在的话),后继节点尝试获取锁。这个过程会一直持续,直到队列为空。
原码-内部类Node
static final class Node {
// 前置节点和后置节点构成双向链表
volatile Node prev;
volatile Node next;
// 线程本身
volatile Thread thread;
// 状态信息,标识节点在同步队列中的等待状态
volatile int waitStatus;
}
源码-获取锁
public final void acquire(int arg) {
// 尝试获取锁失败之后,将线程封装成一个Node节点加入队列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
}
private Node addWaiter(Node mode) {
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;
}
}
// 插入失败时,进入完整的入队操作
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 队列为空,初始化(第一个节点为虚节点,也叫哨兵节点,并不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始的。)
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
acquireQueued() 中方法链路太长,这里拆开讲解。
// 在加入队列排队的同时尝试抢夺资源
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 自旋
for (;;) {
// 获得当前节点的前置节点,判断当前节点是否是头节点。若是则进行抢占锁
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
// 若抢占成功,则设置当前节点为头节点
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 修改Node节点状态,使其线程等待。等候唤醒
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 线程被唤醒,进行自旋判断抢占锁
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// 判断前置节点的 waitStatus 是 SIGNAL 状态
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 获取前置节点的状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 如果是 SIGNAL 状态,即等待被占用的资源释放,直接返回true
return true;
if (ws > 0) {
// 如果 ws 大于0说明是 CANCELLED 状态
do {
// 循环判断前置节点的前置节点是否也为 CANCELLED 状态,忽略该状态的节点,重新连接队列
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 将当前节点的前置节点设置为 SIGNAL 状态,用于后续唤醒操作
// 程序第一次执行到这返回为false,还会进行外层第二次循环
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// 挂起线程,等候唤醒
private final boolean parkAndCheckInterrupt() {
// 线程挂起,程序不会继续向下执行
LockSupport.park(this);
return Thread.interrupted();
}
源码-释放锁
// 尝试释放同步状态。
public final boolean release(int arg) {
// 尝试释放同步状态
if (tryRelease(arg)) {
// 获取当前同步队列的头节点
Node h = head;
// 检查头节点是否存在且其 waitStatus 不为 0(不为 0 表示该节点处于等待状态或取消状态)
if (h != null && h.waitStatus != 0)
// 唤醒头节点的后继节点
unparkSuccessor(h);
return true;
}
return false;
}
// 唤醒指定节点的后继节点。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 清除节点的状态
Node s = node.next; // 获取当前节点的后继节点
if (s == null || s.waitStatus > 0) { // 如果后继节点为 null 或者其状态大于 0(则为1,已取消)
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); // 唤醒后继节点对应的线程
}
条件队列
条件队列与同步队列不同,它通过AQS 中内部类 ConditionObject 实现 Condition 接口,它有两个属性,分别是 firstWaiter(队列的头节点)和 lastWaiter(队列的尾节点),节点类型依然是AQS的内部类Node,通过内部类Node中的 nextWaiter 指针指向下一个等待条件的节点,这样组成一个单向链表。
- 当线程调用了 Condition 的 await() 方法后,它会释放当前持有的锁,并且该线程会被加入到条件队列中等待。
- 然后线程会处于等待状态,直到其他线程调用 signal() 或者 signalAll() 方法来通知条件已经满足。
a. signal() 唤醒等待队列中的头节点对应的线程
b. signalAll() 唤醒队列中所有等待的线程
原码-内部类Node
static final class Node {
// 线程本身
volatile Thread thread;
// 状态信息,标识节点在同步队列中的等待状态
volatile int waitStatus;
// 下一个等待条件的节点
Node nextWaiter;
}
源码-等待
public final void await() throws InterruptedException {
// 如果线程已经被中断,则抛出 InterruptedException
if (Thread.interrupted())
throw new InterruptedException();
// 将当前线程添加到条件队列中
Node node = addConditionWaiter();
// 释放当前线程所持有的锁,并返回释放前的状态,以便以后可以重新获取到相同数量的锁
int savedState = fullyRelease(node);
// 中断模式,用于记录线程在等待过程中是否被中断
int interruptMode = 0;
// 如果当前节点不在同步队列中,则表示线程应该继续等待
while (!isOnSyncQueue(node)) {
// 阻塞当前线程,直到被唤醒或中断
LockSupport.park(this);
// 检查线程是否被中断,并更新 interruptMode 状态
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 当前节点成功加入到同步队列后,尝试以中断模式获取锁
// 如果在此过程中线程被中断,且不是在 signal 之后,则设置中断模式为 REINTERRUPT
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 如果当前节点后面还有等待的节点,从等待队列中清理掉被取消的节点
if (node.nextWaiter != null)
unlinkCancelledWaiters();
// 根据中断模式处理中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
// 将当前线程添加到条件队列中
private Node addConditionWaiter() {
// 获取最后一个等待者
Node t = lastWaiter;
// 如果最后一个等待者已被取消,则清理已取消的等待者
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters(); // 清理已取消的等待者
t = lastWaiter; // 更新最后一个等待者
}
// 创建一个新的条件等待节点
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 如果条件队列为空,则将新节点设置为第一个等待者
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node; // 否则将新节点添加到最后一个等待者的后面
// 更新最后一个等待者为新节点
lastWaiter = node;
// 返回新创建的条件等待节点
return node;
}
源码-唤醒
signal(),唤醒等待队列中的头节点对应的线程
public final void signal() {
// 如果当前线程没有独占锁,则抛出 IllegalMonitorStateException
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取条件队列中的第一个等待者
Node first = firstWaiter;
// 如果存在第一个等待者,则将其移动到同步队列中
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
// 如果条件队列的第一个等待节点的 nextWaiter 为 null,则更新 firstWaiter 和 lastWaiter
if ((firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 清除第一个等待节点的 nextWaiter
first.nextWaiter = null;
} while (!transferForSignal(first) && // 尝试将节点转移到同步队列中并尝试唤醒它
(first = firstWaiter) != null); // 如果还有等待节点,则继续处理
}
// 将指定的条件等待节点从条件队列转移到同步队列中,并尝试唤醒它。
final boolean transferForSignal(Node node) {
// 如果无法更改 waitStatus,则说明节点已被取消
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 将节点插入到同步队列中,并尝试设置前驱节点的 waitStatus 以指示当前线程正在等待
Node p = enq(node);
int ws = p.waitStatus;
// 如果前驱节点的 waitStatus 大于 0 或者设置 waitStatus 失败,则唤醒当前线程
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
// 返回 true 表示转移成功
return true;
}
signalAll(),唤醒等待队列中所有的线程
public final void signalAll() {
// 如果当前线程没有独占锁,则抛出 IllegalMonitorStateException
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取条件队列中的第一个等待者
Node first = firstWaiter;
// 如果存在第一个等待者,则将所有等待者移动到同步队列中
if (first != null)
doSignalAll(first);
}
private void doSignalAll(Node first) {
// 清空条件队列的头部和尾部指针
lastWaiter = firstWaiter = null;
// 循环处理条件队列中的所有等待节点
do {
// 获取当前节点的下一个等待节点
Node next = first.nextWaiter;
// 清除当前节点的 nextWaiter
first.nextWaiter = null;
// 将当前节点转移到同步队列中,并尝试唤醒它
transferForSignal(first);
// 更新当前节点为下一个等待节点
first = next;
} while (first != null); // 继续处理直到条件队列为空
}
Demo-比较典型的条件队列使用场景
小编编写了一个比较典型的条件队列使用场景 ReentrantLock 的 Condition,大家可以通过debug该Demo查看源码,便于理解。
package com.gw.tool.demos.concurrent;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* Description: ReentrantLock.Condition使用场景Demo
*
* @author YanAn
* @date 2023/8/10 13:52
*/
public class ReentrantLockDemo {
private final ReentrantLock lock = new ReentrantLock();
private final Condition condition1 = lock.newCondition();
private final Condition condition2 = lock.newCondition();
public void awaitAndSignal() throws InterruptedException {
lock.lock();
try {
// 等待条件唤醒
System.out.println(Thread.currentThread().getName() + ",等待条件1唤醒");
condition1.await();
System.out.println(Thread.currentThread().getName() + ",满足条件1被唤醒");
System.out.println(Thread.currentThread().getName() + ",等待条件2唤醒");
condition2.await();
System.out.println(Thread.currentThread().getName() + ",满足条件1被唤醒");
} finally {
lock.unlock();
}
}
public void signalCondition1() throws InterruptedException {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + ",唤醒等待条件1的线程");
condition1.signal(); // 唤醒等待的线程
} finally {
lock.unlock();
}
}
public void signalCondition2() throws InterruptedException {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + ",唤醒等待条件2的线程");
condition2.signal(); // 唤醒等待的线程
} finally {
lock.unlock();
}
}
public static void main(String[] args) throws InterruptedException {
ReentrantLockDemo demo = new ReentrantLockDemo();
Thread threadA = new Thread(() -> {
try {
demo.awaitAndSignal();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread threadB = new Thread(() -> {
try {
demo.signalCondition1();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread threadC = new Thread(() -> {
try {
demo.signalCondition2();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
threadA.start();
Thread.sleep(100); // 确保线程A先运行
threadB.start();
Thread.sleep(100);
threadC.start();
}
}