Java 中的 CLH 队列自旋锁与 JDK 17 的实现
CLH 队列自旋锁是一种基于队列的自旋锁机制,广泛应用于 Java 并发工具类中,尤其是在 JDK 的 AbstractQueuedSynchronizer (AQS) 框架中。它通过队列管理线程的等待和唤醒,减少了竞争锁时的资源消耗。
CLH 队列的基本原理
CLH 锁是一种 FIFO(先进先出)队列锁,通过 CAS 操作将线程节点加入队列尾部。线程只需在其前驱节点上自旋,等待前驱节点释放锁即可。相比传统自旋锁,CLH 锁减少了 CAS 操作的频率,避免了 CPU 总线风暴。
在 CLH 队列中,每个节点包含以下状态:
- CANCELLED (1) :线程因超时或中断被取消,不再参与锁竞争。
- SIGNAL (-1) :后继节点需要被唤醒。
- CONDITION (-2) :线程处于条件队列中,等待特定条件唤醒。
- 0:初始状态,表示新建节点。
CLH 队列在 AQS 中的实现
在 JDK 17 中,AQS 使用 CLH 队列管理线程的同步。以下是其核心机制:
入队操作
当线程尝试获取锁失败时,会通过 addWaiter 方法将线程节点加入队列尾部:
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;
}
通过 CAS 操作确保线程安全,若失败则自旋重试。
自旋与阻塞
线程在前驱节点上自旋,判断其状态是否为 false。若前驱节点释放锁,当前线程即可获取锁;否则进入阻塞状态,等待被唤醒:
for (;;) {
if (node.prev == head && tryAcquire(arg)) {
head = node;
node.prev = null; // 释放前驱节点
return;
}
LockSupport.park(this); // 阻塞当前线程
}
释放锁
释放锁时,当前线程会唤醒其后继节点:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
通过 unparkSuccessor 方法唤醒等待的线程。
CLH 队列的优缺点
优点:
- 减少 CAS 操作:线程仅在入队时使用 CAS 操作,后续只需普通自旋。
- 空间复杂度低:需要的存储空间为 O(L+N),其中 L 为锁数量,N 为线程数量。
缺点:
- NUMA 架构性能较差:在 NUMA 架构中,前驱节点的内存位置可能较远,导致性能下降。
- 适合 SMP 架构:在对称多处理器(SMP)架构中性能较高。
在 JDK 17 中的应用
JDK 17 中,AQS 基于 CLH 队列实现了多种同步器,如 ReentrantLock、Semaphore 和 CountDownLatch。这些同步器通过重写 AQS 的模板方法(如 tryAcquire 和 tryRelease)实现特定的同步逻辑。
例如,ReentrantLock 的非公平锁实现:
static final class NonfairSync extends Sync {
final boolean initialTryLock() {
Thread current = Thread.currentThread();
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(current);
return true;
}
return false;
}
}
通过 CLH 队列和 AQS 的结合,JDK 提供了高效、灵活的并发工具,适用于多种场景。