CLH锁(Craig, Landin, and Hagersten Locks (CLH Locks)) 是一种基于链表的自旋锁,采用类似队列的方式管理线程的排队。该锁的核心思想是通过在每个线程节点中存储锁的状态来确保线程的顺序访问,而不直接依赖于共享的状态。CLH 锁有效减少了线程竞争时的缓存一致性问题。
下面是一个简单的 Java 实现,模拟了 CLH 锁的工作原理。
CLH 锁的核心思想
- 每个线程都有一个自己的节点 (
CLHNode
),这个节点包含一个locked
标志位,表示该线程是否正在持有锁。 tail
是指向队列末尾的指针,AtomicReference
用于确保操作的原子性。- 当线程试图获取锁时,会自旋等待直到其前驱节点释放锁。
- 线程解锁时,仅将自己的
locked
标志设置为false
,并不通知后继线程,后继线程通过检查前驱节点的状态来判断是否可以获得锁。
CLH 锁的Java实现
import java.util.concurrent.atomic.AtomicReference;
public class CLHLock {
// 每个线程有一个自己的节点
private ThreadLocal<CLHNode> myNode;
// 当前队列的尾部
private AtomicReference<CLHNode> tail;
// CLH锁节点类
static class CLHNode {
volatile boolean locked = false; // 线程是否持有锁,默认false,表示不持有锁
}
public CLHLock() {
tail = new AtomicReference<>(null); // 初始时队列为空
myNode = ThreadLocal.withInitial(CLHNode::new); // 每个线程都有自己的节点
}
// 上锁方法
public void lock() {
CLHNode node = myNode.get(); // 获取当前线程的节点
node.locked = true; // 设置节点为锁定状态
// 将当前节点设置为队列尾部
CLHNode predecessor = tail.getAndSet(node); // 获取当前队尾节点,并将当前节点设置为新尾节点
// 如果前驱节点仍在锁定状态,当前线程进入自旋等待
if (predecessor != null) {
while (predecessor.locked) {
// 自旋等待前驱线程释放锁
}
}
}
// 解锁方法
public void unlock() {
CLHNode node = myNode.get(); // 获取当前线程的节点
node.locked = false; // 设置节点为解锁状态
// 不需要通知后继线程,后继线程通过检查前驱节点的状态来判断是否可以获得锁
}
public static void main(String[] args) throws InterruptedException {
final CLHLock lock = new CLHLock();
// 多线程测试
Runnable task = () -> {
lock.lock();
try {
// 临界区代码
System.out.println(Thread.currentThread().getName() + " is in critical section.");
} finally {
lock.unlock();
}
};
// 创建多个线程并执行
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(task);
threads[i].start();
}
// 等待所有线程执行完毕
for (int i = 0; i < 5; i++) {
threads[i].join();
}
}
}
代码解析:
-
CLHNode 类:
locked
字段表示该线程是否持有锁。默认情况下,locked
是false
,表示该线程未获得锁。- 每个线程有自己的
CLHNode
节点,节点存储了当前线程的锁状态。
-
CLHLock 类:
- 使用
AtomicReference<CLHNode>
作为队列的尾部tail
,确保对尾部的操作是原子性的。 ThreadLocal<CLHNode>
为每个线程分配一个独立的节点,避免多个线程共享同一个节点。
- 使用
-
lock() 方法:
- 当前线程获取锁时,首先将其自己的节点的
locked
标志设置为true
,然后尝试将该节点添加到队列的尾部。 - 通过
tail.getAndSet(node)
原子性地将当前节点设置为队列的尾部,并返回之前的尾部节点(即前驱节点)。 - 如果前驱节点的
locked
标志仍然是true
,当前线程会自旋等待直到前驱节点释放锁。
- 当前线程获取锁时,首先将其自己的节点的
-
unlock() 方法:
- 当前线程释放锁时,简单地将
locked
标志设置为false
。由于 CLH 锁的结构,后继线程会通过检查前驱节点的状态来决定是否可以进入临界区,因此无需显式地通知后继线程。
- 当前线程释放锁时,简单地将
-
main 方法:
- 创建了 5 个线程,并让它们并发地执行相同的任务。每个线程在临界区执行时会首先调用
lock()
获取锁,完成后调用unlock()
释放锁。
- 创建了 5 个线程,并让它们并发地执行相同的任务。每个线程在临界区执行时会首先调用
输出实例
参考
CLH lock queue的原理解释及Java实现 - 简书
https://juejin.cn/post/7329457173912698895