CLH Lock & MCS Lock

本文对比分析了CLH锁与MCS锁这两种公平锁的实现原理及使用方式。CLH锁与MCS锁均采用自旋轮询机制确保公平性,但它们在节点状态的维护上有显著不同:CLH锁关注前一个节点状态,而MCS锁关注的是后一个节点状态。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


共性: 都是公平锁,先进先出,通过自旋轮询状态

区别: CLH锁持有前个节点状态并轮询,MCS持有后一个节点状态并轮询。


0,节点类 QNode

package com.test.learn;

public class LockNode {
    /**
     * 标志锁的状态
     */
    volatile boolean locked = false;

    /**
     * 用于保存后个节点
     */
    volatile LockNode next = null;
}



1,CLHLock

package com.test.learn;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class CLHLock implements Lock {

    // 提供CAS方式修改地址引用
    private AtomicReference<LockNode> tail;
    // 保存前一个节点引用
    private ThreadLocal<LockNode> myPre;
    // 当前节点
    private ThreadLocal<LockNode> myNode;

    public CLHLock() {
        tail = new AtomicReference<>(new LockNode());
        myNode = ThreadLocal.withInitial(LockNode::new);
        myPre = ThreadLocal.withInitial(() -> null);
    }

    @Override
    public void lock() {
        // L1.将自身节点状态标志为true(已上锁)
        LockNode node = myNode.get();
        node.locked = true;
        // L2.获取前个节点
        LockNode pre = tail.getAndSet(node);
        myPre.set(pre);
        while (pre.locked) {
            // L3.自旋等待前个节点释放锁,即前一个节点执行unLock(U2处)
        }
    }

    @Override
    public void unlock() {
        // 获取当前节点
        LockNode qnode = myNode.get();
        // 通过状态标志为锁已释放
        qnode.locked = false;
        // 回收前驱节点
        myNode.set(myPre.get());
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        // TODO
    }

    @Override
    public boolean tryLock() {
        // TODO
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        // TODO
        return false;
    }

    @Override
    public Condition newCondition() {
        // TODO
        return null;
    }
}

2,MCSLock

package com.test.learn;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

public class MCSLock implements Lock {

    // 提供CAS方式修改地址引用
    private AtomicReference<LockNode> tail;
    // 保存当前节点
    private ThreadLocal<LockNode> myNode;

    public MCSLock() {
        // tail和CLH有区别,初始值为null
        tail = new AtomicReference<>();
        myNode = ThreadLocal.withInitial(LockNode::new);
    }

    @Override
    public void lock() {
        LockNode qnode = myNode.get();
        // L1,获取前节点
        LockNode pre = tail.getAndSet(qnode);
        // L2.不存在前一个节点则直接获取锁了,否则自旋前节点的状态
        if (null != pre) {
            qnode.locked = true;
            // L3 设置后续节点
            pre.next = qnode;
            while (qnode.locked) {
                // 自旋前节点的状态
            }
        }
    }

    @Override
    public void unlock() {
        LockNode qnode = myNode.get();
        if (qnode.next == null) {
            // 为什么需要CAS判断是否更新成功?因为多线程下可能新增了新的节点。
            if (tail.compareAndSet(qnode, null)) {
                return;
            }
            // 循环到等到next有值后退出,也即后一个节点执行L3之后
            while (qnode.next == null) {
            }
        }
        // 解锁时候主动更新后节点状态(和CLH的主要差距点)
        qnode.next.locked = false;
        qnode.next = null;
    }


    @Override
    public void lockInterruptibly() throws InterruptedException {
        // TODO
    }

    @Override
    public boolean tryLock() {
        // TODO
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        // TODO
        return false;
    }

    @Override
    public Condition newCondition() {
        // TODO
        return null;
    }
}

3, 使用

package com.test.learn;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.locks.Lock;

public class LockMain {

    private static final int THEAD_CNT = 10;

    private static Lock getLock() {
        // return new MCSLock();
        return new CLHLock();
    }

    public static void main(String[] args) throws Exception {
        CyclicBarrier cb = new CyclicBarrier(THEAD_CNT);
        CountDownLatch latch = new CountDownLatch(THEAD_CNT);

        Lock lock = getLock();
        for (int i = 0; i < THEAD_CNT; i++) {
            Thread th = new Thread(() -> {
                try {
                    cb.await();
                    System.out.println(Thread.currentThread().getName());
                    lock.lock();
                    System.out.println(Thread.currentThread().getName() + " Got the Lock!");
                    Thread.sleep(100);
                    lock.unlock();
                    System.out.println(Thread.currentThread().getName() + " Released the Lock!");
                    latch.countDown();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "Thread:" + i);
            th.start();
        }

        long start = System.currentTimeMillis();
        latch.await();
        System.out.println("Duration:" + (System.currentTimeMillis() - start));
    }
}


### CLH队列自旋锁的详细原理及实现 #### 1. CLH队列锁的基本概念 CLH(Craig, Landin, and Hagersten)队列锁是一种基于链表结构的公平自旋锁。当多个线程竞争同一把锁时,获取不到锁的线程会被组织成一个单向链表[^1]。每个线程在自己的节点上设置状态标志,并在其前驱节点的状态标志上进行自旋等待,直到前驱节点释放锁。 #### 2. 工作原理 CLH队列锁的核心思想是通过链表结构将等待锁的线程串联起来,形成一个虚拟队列。每个线程在尝试获取锁时会创建一个节点并插入到队列尾部。具体工作流程如下: - **锁的获取**:线程在尝试获取锁时,首先检查前驱节点的状态标志。如果前驱节点的状态标志为`false`,表示前驱节点已经释放了锁,当前线程可以获取锁[^1]。 - **锁的释放**:当线程完成临界区操作后,将自身的状态标志设置为`false`,从而通知其后继线程可以获取锁[^1]。 这种设计避免了惊群效应,因为锁释放后只会唤醒队列中的下一个线程[^1]。 #### 3. CLH队列锁的优点 - **无惊群效应**:锁释放后,只有队列中的第一个等待线程会被唤醒,其他线程继续自旋等待[^1]。 - **空间复杂度低**:CLH队列锁的空间复杂度为O(L+n),其中L是锁的数量,n是线程的数量。 - **公平性**:由于使用了队列结构,线程按照进入队列的顺序获取锁,保证了公平性。 #### 4. CLH队列锁的缺点 - **NUMA系统下的性能问题**:在NUMA(Non-Uniform Memory Access)系统中,线程可能需要访问远程内存来读取前驱节点的状态标志,这会导致性能下降[^1]。 - **自旋开销**:在高竞争场景下,线程可能会长时间自旋,消耗大量CPU资源[^3]。 #### 5. CLH队列锁的实现 以下是CLH队列锁的一个简单实现示例: ```java public class CLHLock { private static class Node { volatile boolean locked = true; } private final ThreadLocal&lt;Node&gt; nodeRef = ThreadLocal.withInitial(Node::new); private volatile Node tail; public void lock() { Node node = nodeRef.get(); node.locked = true; Node pred = null; while (true) { pred = tail; if (pred != null &amp;&amp; casTail(pred, node)) { break; } } while (pred != null &amp;&amp; pred.locked) { // 自旋等待前驱节点释放锁 } } public void unlock() { Node node = nodeRef.get(); node.locked = false; // 如果没有后继节点,则将tail置为null if (!nodeRef.get().locked) { casTail(node, null); } } private boolean casTail(Node expect, Node update) { return java.util.concurrent.atomic.AtomicReferenceFieldUpdater.newUpdater( CLHLock.class, Node.class, &quot;tail&quot;).compareAndSet(this, expect, update); } } ``` #### 6. CLH队列锁与MCS队列锁的对比 CLH队列锁和MCS(Mellor-Crummey and Scott)队列锁的主要区别在于线程自旋的规则不同。CLH是在前驱节点的`locked`域上自旋等待,而MCS是在自己的节点的`locked`域上自旋等待[^2]。这种差异使得MCS锁更适合NUMA架构,因为它减少了对远程内存的访问[^2]。 #### 7. Java中的应用 Java中的`ReentrantLock`重入锁中的AQS(AbstractQueuedSynchronizer)队列排队策略是基于CLH队列的一种变种实现。原始的CLH队列一般用于实现自旋锁,而AQS队列的实现则是获取不到锁的线程先进行一小段时间的自旋,然后进入`Park`挂起状态。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值