Java 中的 CLH 队列自旋锁与 JDK 17 的实现

3 阅读2分钟

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 队列实现了多种同步器,如 ReentrantLockSemaphore 和 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 提供了高效、灵活的并发工具,适用于多种场景。