CLH锁与MCS锁

32 篇文章 0 订阅
18 篇文章 0 订阅

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的实现。上一个版本对于每个线程都缓存一个当前节点与前置节点。参考下面的公式显然优化后的空间复杂度更低
优化前:
一把锁的空间复杂度=并发线程数 * 2threadlocal引用 +并发线程数 * 1locked标识+1tail引用
优化后:
一把锁的空间复杂度=并发线程数 * 1next引用+并发线程数 * 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

简单的讲就是:CLH每次都要大老远的跑去问上一个节点。MCS只需要上一个节点释放的时候跑一次,更新子节点的状态(通知它锁释放了)

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系统的重要差别)。显然,访问本地内存的速度将远远高于访问远地内存 ( 系统内其它模块的内存 )

图片取自网络
SMP.jpg

NUMA

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值