一、队列实现原理
1、同步等待队列和条件等待队列
[
二、同步等待队列
主要用于维护获取锁失败时入队的线程
1、举个例子🌰🌰
1.1、代码
import java.util.concurrent.locks.ReentrantLock;
/**
* 模拟购买场景
*/
public class ReentrantLockDemo {
//默认非公平
private final ReentrantLock lock = new ReentrantLock();
// 总数
private static int count = 8;
/**
* 模拟购买
*/
public void buy() {
// 获取锁
lock.lock();
try {
if (count > 0) {
try {
// 休眠1s
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":购买了第" + count-- + "张,同步队列长度"+ lock.getQueueLength());
} else {
System.out.println(Thread.currentThread().getName() + ":购买失败,同步队列长度"+ lock.getQueueLength());
}
} finally {
// 释放锁
lock.unlock();
}
}
public static void main(String[] args) {
ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
for (int i = 1; i <= 10; i++) {
Thread thread = new Thread(() -> {
reentrantLockDemo.buy(); // 抢票
}, "线程" + i);
// 启动线程
thread.start();
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("剩余:" + count);
}
}
1.2、结果
线程1:购买了第8张,同步队列长度9
线程2:购买了第7张,同步队列长度8
线程3:购买了第6张,同步队列长度7
线程4:购买了第5张,同步队列长度6
线程5:购买了第4张,同步队列长度5
线程6:购买了第3张,同步队列长度4
线程7:购买了第2张,同步队列长度3
线程8:购买了第1张,同步队列长度2
线程9:购买失败,同步队列长度1
线程10:购买失败,同步队列长度0
剩余:0
⚠️注意:
✔️负责管理在竞争锁失败时进入等待状态的线程,基于双向链表数据结构的队列,是FIFO先进先出线程等待队列,Java中的[CLH队列]是原CLH队列的一个变种,线程由原自旋机制改为阻塞机制。
2、源码分析
2.1、源码
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
// 将当前线程封装为一个独占模式的节点(Node),并添加到AQS的同步队列尾部。
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;
}
// 一个无限循环,用于让线程持续尝试获取资源
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
//自旋等待逻辑
for (;;) {
final Node p = node.predecessor();
//判断当前节点的前驱节点是否为头节点(head),并且尝试获取锁
if (p == head && tryAcquire(arg)) {
//如果成功获取资源,则更新头节点、断开旧节点链接,并返回中断状态。
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
//挂起线程等待唤醒
//如果未能获取资源,则根据前驱节点状态决定是否挂起线
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
📌 for (;;) 的核心意义:
✔️自旋等待:头节点对应的线程会在队列中循环尝试获取锁,直到成功获取锁或被中断。这种方式避免了频繁的线程阻塞与唤醒带来的内核态切换开销,从而显著提升并发性能。即便线程被挂起后再次唤醒,也会重新进入自旋逻辑继续竞争锁。
✔️灵活控制:通过循环条件动态判断是否可以获取资源或需要挂起线程。支持中断响应,增强程序的健壮性。
✔️高效资源管理:成功获取资源后立即退出循环,减少不必要的计算。
三、条件等待队列
- 调用await() 的时候会释放锁,然后线程会加入到条件队列
- 调用signal() 唤醒的时候会把条件队列中的线程节点移动到同步队列中,等待再次获得锁
1、举个例子🌰🌰
1.1、代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionDemo {
/**
* 锁
*/
private static ReentrantLock lock = new ReentrantLock();
/**
* 判断条件
*/
private static Condition condition = lock.newCondition();
/**
* 条件标志位
*/
private static volatile boolean flag = false;
public static void main(String[] args) throws InterruptedException {
new Thread(new DemoOne(), "DemoOne").start();
Thread.sleep(1000);
new Thread(new DemoTwo(), "DemoTwo").start();
}
/**
* DemoOne
*/
static class DemoOne implements Runnable {
@Override
public void run() {
lock.lock();
try {
while (!flag) {
System.out.println(Thread.currentThread().getName() + "当前条件不满足等待");
try {
// TODO:something
condition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + "接收到通知条件满足");
} finally {
lock.unlock();
}
}
}
/**
* DemoTwo
*/
static class DemoTwo implements Runnable {
@Override
public void run() {
lock.lock();
try {
flag = true;
System.out.println(Thread.currentThread().getName() + "唤醒DemoOne");
condition.signal();
} finally {
lock.unlock();
}
}
}
}
1.2、结果
DemoOne当前条件不满足等待
DemoTwo唤醒DemoOne
DemoOne接收到通知条件满足
⚠️注意:
✔️条件等待队列会把条件等待队列中的线程节点移动到同步队列中,等待再次获得锁
✔️如果同步等待队列为空时,会唤醒条件等待队列的第一个节点(node)
2、源码分析
2.1,源码
// 如果存在等待时间最长的线程,则将其从该条件的等待队列移至拥有该锁的等待队列
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
// 移除并转移节点,直到遇到未被取消的节点或空值
private void doSignal(Node first) {
do {
//将 first.nextWaiter 赋值给 firstWaiter,即让 firstWaiter 指向当前节点的下一个等待者
//如果 first.nextWaiter 为 null,说明当前节点是队列中最后一个节点,因此将 lastWaiter 设置为 null,表示队列为空
if ( (firstWaiter = first.nextWaiter) == null)
//断开当前节点的链接
lastWaiter = null;
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
// 尝试唤醒当前节点
// 如果 firstWaiter 为 null,说明队列已遍历完毕,退出循环
}
// 将节点从条件队列转移到同步队列
final boolean transferForSignal(Node node) {
//使用 CAS 操作将节点的 waitStatus 从 Node.CONDITION 改为 0。
//如果失败,说明该节点已经被取消(cancelled),直接返回 false。
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
//调用 enq(node) 将节点加入同步队列(Sync Queue)。
//返回值 p 是该节点在同步队列中的前驱节点。
Node p = enq(node);
int ws = p.waitStatus;
//获取前驱节点 p 的 waitStatus。
//如果前驱节点的状态大于 0(表示已取消),或者无法将其状态设置为 Node.SIGNAL,则直接唤醒当前节点的线程。
//否则,依赖前驱节点来唤醒当前节点。
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
return true;
}
⚠️注意
✔️基于LockSupport.unpark(node.thread)、LockSupport.park(node.thread) 实现阻塞和唤醒
✔️条件队列在AQS内部是一个单向链表,节点在被转移到同步队列时,被构建为双向链表节点CLH队列
四、总结
1、ReentrantLock 基于 AQS+CAS 实现了同步队列与条件队列两大核心结构:
- 同步队列: 负责管理抢锁失败的线程,通过头节点自旋减少线程切换开销,提升并发性能;
- 条件队列:用于实现精准的等待 / 通知机制,一个锁可以支持多个条件队列,解决了传统通知无法精准唤醒的问题。
[
2、ReentrantLock 与synchronized相比:
- synchronized:只有一套隐式的等待队列,使用简单但功能单一;
- ReentrantLock :依靠同步队列 + 条件队列的配合,在锁的灵活性、可控性、并发调度精度上都更加强大,是复杂并发场景下更优的选择。