欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

CLH锁与MCS锁

程序员文章站 2022-05-05 10:00:07
...

Ticket锁(传送门)的缺点在于扩展性,cpu不断增加扩展时,对于共享变量(当前叫号叫到了几号)的访问会成为系统的瓶颈,共享变量递增导致大量的线程本地缓存被置为无效,需要通过内存总线访问主存刷新本地缓存,会造成拥塞。而CLH锁与MCS锁正式为了解决此瓶颈而衍生的算法。下面我来看一下他们的实现以及对比。

CLH锁

定义

CLH锁是一个可扩展,高性能,公平的基于链表的自旋锁,应用线程仅自旋在本地变量,它一直读取前置节点的状态,如果它发现前置节点释放了锁则结束自旋

实现

import java.util.concurrent.atomic.AtomicReference;
import lombok.ToString;

/**
 * @author 会灰翔的灰机
 * @date 2019/9/21
 */
public class CLHLock implements MyLock {
    AtomicReference<CLHNode> tail;
    ThreadLocal<CLHNode> myPred;
    ThreadLocal<CLHNode> myNode;
    @ToString
    private static class CLHNode {
        private volatile boolean locked;
    }
    public CLHLock() {
        tail = new AtomicReference<>(new CLHNode());
        myNode = ThreadLocal.withInitial(CLHNode::new);
        myPred = ThreadLocal.withInitial(() -> null);
    }

    @Override
    public void lock() {
        CLHNode clhNode = myNode.get();
        clhNode.locked = true;
        CLHNode pred = tail.getAndSet(clhNode);
        myPred.set(pred);
        while (pred.locked) {
            System.out.println("spin...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void unlock() {
        CLHNode clhNode = myNode.get();
        clhNode.locked = false;
        myNode.set(myPred.get());
    }

    public static void main(String[] args) {
        CLHLock clhLock = new CLHLock();
        MyTask myTask = new MyTask(3000, clhLock);
        myTask.start();
        MyTask myTask2 = new MyTask(1000, clhLock);
        myTask2.start();
        MyTask myTask3 = new MyTask(1000, clhLock);
        myTask3.start();
    }
}

CLH锁优化版,参考AQS的实现,上一个版本对于每个线程都缓存一个当前节点与前置节点,一把锁的空间复杂度=2并发线程数的threadlocal引用+并发线程数1locked标识+1tail引用,下面这个版本一把锁的空间复杂度=并发线程数的next引用+并发线程数*1locked标识+1tail指针+1head指针。显然优化后的空间复杂度更低

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import lombok.ToString;

/**
 * @author 会灰翔的灰机
 * @date 2019/9/21
 */
public class CLHLock implements MyLock {

    @ToString
    private static class CLHNode {
        /**
         * 当前节点是否已释放,释放说明下一个节点可以获取锁,否则则进入自旋
         */
        private volatile boolean released;
        private volatile CLHNode next;

        boolean isReleased() {
            return released;
        }
        void release(){
            released = true;
        }
    }

    private volatile CLHNode tail;
    private volatile CLHNode head;
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> HEAD_UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class, "head");
    private static final AtomicReferenceFieldUpdater<CLHLock, CLHNode> TAIL_UPDATER = AtomicReferenceFieldUpdater.newUpdater(CLHLock.class, CLHNode.class, "tail");
    /**
     * 记录锁持有者,只有持有者才能解锁
     */
    private ThreadLocal<Thread> threadLocal = new ThreadLocal<>();

    @Override
    public void lock() {
        CLHNode pre;
        for(;;) {
            CLHNode clhNode = new CLHNode();
            // 1. 第一个节点初始化
            if (tail == null) {
                // 第一个节点不需要等待直接获取锁资源
                clhNode.release();
                if (HEAD_UPDATER.compareAndSet(this, null, clhNode)) {
                    tail = head;
                }
            } else {
                // 2. 非第一个节点,需要等待锁持有者释放资源,添加等待节点
                CLHNode lastTail = tail;
                // 追加等待节点
                if (TAIL_UPDATER.compareAndSet(this, tail, clhNode)) {
                    // 更新next指针,锁获取成功时,需要使用next指针将head节点更新为下一个等待节点
                    lastTail.next = clhNode;
                    // 前置节点缓存并遍历其状态判断是否可以终止自旋执行代码
                    pre = lastTail;
                    break;
                }
            }
        }
        // 3. 自旋在本地变量pre上,遍历前置节点的状态,判断是否锁资源已经释放,是则终止自旋执行代码
        while(!pre.isReleased()) {
            System.out.println("spin..." + pre);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 4. 自旋结束获取锁成功,head节点状态已经是释放状态,将head更新为下一个等待节点
        while (!HEAD_UPDATER.compareAndSet(this, head, head.next)) {
            System.out.println("head节点更新失败,稍候重试");
        }
        threadLocal.set(Thread.currentThread());
    }
    @Override
    public void unlock(){
        // 5. 更新头节点状态为释放
        if (head != null && Thread.currentThread() == threadLocal.get()) {
            head.release();
        }
        threadLocal.remove();
        System.out.println("head : " + head);
    }

    public static void main(String[] args) {
        CLHLock clhLock = new CLHLock();
        MyTask myTask = new MyTask(3000, clhLock);
        myTask.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        clhLock.unlock();
        MyTask myTask2 = new MyTask(1000, clhLock);
        myTask2.start();
        MyTask myTask3 = new MyTask(1000, clhLock);
        myTask3.start();
    }
}

MCS锁

定义

与CLH锁类型,锁的实现结合了队列或链表。区别在于CLH自旋在前置节点的状态。MCS锁自旋在当前节点的状态。

实现

package com.gallant.dispatch.juc;

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import lombok.ToString;

/**
 * @author 会灰翔的灰机
 * @date 2019/9/21
 */
public class MCSLock implements MyLock {

    @ToString
    private static class MyNode {

        /**
         * 当前节点是否已释放,释放说明下一个节点可以获取锁,否则则进入自旋
         */
        private volatile boolean released;
        private volatile MyNode next;

        boolean isReleased() {
            return released;
        }

        void release() {
            released = true;
        }
    }

    private volatile MyNode tail;
    private static final AtomicReferenceFieldUpdater<MCSLock, MyNode> TAIL_UPDATER = AtomicReferenceFieldUpdater
            .newUpdater(MCSLock.class, MyNode.class, "tail");
    private ThreadLocal<Thread> owner = new ThreadLocal<>();
    private ThreadLocal<MyNode> currentNodeCache = new ThreadLocal<>();

    @Override
    public void lock() {
        MyNode currentNode;
        for(;;) {
            currentNode = new MyNode();
                MyNode lastTail = tail;
                if (TAIL_UPDATER.compareAndSet(this, tail, currentNode)) {
                    if (lastTail == null) {
                        currentNode.release();
                    } else {
                        lastTail.next = currentNode;
                    }
                    currentNodeCache.set(currentNode);
                    break;
                }
        }
        while (!currentNode.isReleased()) {
            System.out.println("mcslock spin..." + currentNode);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        owner.set(Thread.currentThread());
    }

    @Override
    public void unlock() {
        MyNode currentNode = currentNodeCache.get();
        if (currentNode != null && currentNode.next != null && Thread.currentThread() == owner
                .get()) {
            currentNode.next.release();
            currentNodeCache.remove();
            owner.remove();
        }
        System.out.println("mcslock head:" + currentNodeCache);
    }

    public static void main(String[] args) {
        MCSLock mcsLock = new MCSLock();
        MyTask myTask = new MyTask(3000, mcsLock);
        myTask.start();
        MyTask myTask2 = new MyTask(1000, mcsLock);
        myTask2.start();
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mcsLock.unlock();
        MyTask myTask3 = new MyTask(1000, mcsLock);
        myTask3.start();
    }
}

对比

区别上面已经说过了,此处不再赘述。直接聊聊他们的优缺点与使用场景

CLHLock、MCSLock

共同的优点:无饥饿、公平性、可扩展性高、高性能(仅需要一次CAS操作,setHead操作由于是在锁内所以可以删除上文中多出来的一次CAS)
CLHLock缺点:缺点是在NUMA(cpu对称架构)系统结构下性能非常差。在这样的系统结构下,对于每个cpu其他资源(内存、io)都是共享的,每一个线程有自己的内存,假设前趋结点的内存位置比较远。自旋访问前趋结点的locked域(内存地址),性能将大打折扣,可是在SMP系统结构下该法还是非常有效的。MCSLock自旋在本地节点,每次访问的都是本地内存,所以不存在CLHLock的内存位置比较远的问题。
当锁节点状态发生变化时,CLHLock只需要更新本地状态,子节点需要同步刷新缓存,一次同步动作可能会存在子节点内存比较远的问题。MCSLock则需要更新子节点的状态,一次写动作可能会存在子节点内存比较远的问题。但是CLHLock每次自旋因为是轮询的前置节点状态会存在内存比较远的问题。MCSLock自旋在自身节点状态,自旋的场景不存在内存比较远的问题。所以CPU的架构在NUMA系统结构下性能优于CLHLock

CPU SMP、NUMA架构

SMP(Symmetric Multi-Processor)

对称多处理器架构,是一种cpu架构设计模式,服务器中多个CPU对称工作。SMP服务器的主要特征是共享,系统资源(CPU 、内存、 I/O 等 )都是共享的。由于这种特征,导致SMP服务器的扩展能力受到约束。每一个共享的环节都可能成为SMP服务器的扩展瓶颈,而最受限制的是内存。每个CPU必须通过内存总线访问相同的内存资源,随着CPU数量增加,内存访问冲突将随着增加,最终导致CPU性能大打折扣

NUMA(Non-Uniform Memory Access)

NUMA服务器的基本特征是具有多个CPU模块,每个CPU模块由多个CPU组成,具有独立的本地内存、I/O槽口等。其节点之间可以通过互联模块进行连接和信息交互,因此每个CPU可以访问整个系统的内存( NUMA系统与MPP系统的重要差别)。显然,访问本地内存的速度将远远高于访问远地内存 ( 系统内其它模块的内存 )

CLH锁与MCS锁