zoukankan      html  css  js  c++  java
  • CLH lock 原理及JAVA实现

     

     --喜欢记得关注我哟【shoshana】--

     

    前记

    JUC中的Lock中最核心的类AQS,其中AQS使用到了CLH队列的变种,故来研究一下CLH队列的原理及JAVA实现

    一. CLH背景知识

    SMP(Symmetric Multi-Processor)。即对称多处理器结构,指server中多个CPU对称工作,每一个CPU訪问内存地址所需时间同样。其主要特征是共享,包括对CPU,内存,I/O等进行共享。SMP的长处是可以保证内存一致性。缺点是这些共享的资源非常可能成为性能瓶颈。随着CPU数量的添加,每一个CPU都要訪问同样的内存资源,可能导致内存訪问冲突,可能会导致CPU资源的浪费。经常使用的PC机就属于这样的。
    NUMA(Non-Uniform Memory Access)非一致存储訪问,将CPU分为CPU模块,每一个CPU模块由多个CPU组成,而且具有独立的本地内存、I/O槽口等,模块之间能够通过互联模块相互訪问,訪问本地内存的速度将远远高于訪问远地内存(系统内其他节点的内存)的速度,这也是非一致存储訪问NUMA的由来。NUMA长处是能够较好地解决原来SMP系统的扩展问题,缺点是因为訪问远地内存的延时远远超过本地内存,因此当CPU数量添加时。系统性能无法线性添加。

    CLH 锁的名字也与他们的发明人的名字相关:Craig,Landin and Hagersten。

    CLH Lock摘要

    CLH lock is Craig, Landin, and Hagersten (CLH) locks, CLH lock is a spin lock, can ensure no hunger, provide fairness first come first service.
    The CLH lock is a scalable, high performance, fairness and spin lock based on the list, the application thread spin only on a local variable, it constantly polling the precursor state, if it is found that the pre release lock end spin.

    CLH锁是自旋锁的一种,对它的研究是因为AQS源代码中使用了CLH锁的一个变种,为了更好的理解AQS中使用锁的思想,所以决定先好好理解CLH锁

    二. CLH原理

    CLH也是一种基于单向链表(隐式创建)的高性能、公平的自旋锁,申请加锁的线程只需要在其前驱节点的本地变量上自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。

    三. Java代码实现

    类图

     

    
    
    public interface Lock {
        void lock();
    
        void unlock();
    }
    
    public class QNode {
        volatile boolean locked;
    }
    
    
    import java.util.concurrent.atomic.AtomicReference;
    
    public class CLHLock implements Lock {
        // 尾巴,是所有线程共有的一个。所有线程进来后,把自己设置为tail
        private final AtomicReference<QNode> tail;
        // 前驱节点,每个线程独有一个。
        private final ThreadLocal<QNode> myPred;
        // 当前节点,表示自己,每个线程独有一个。
        private final ThreadLocal<QNode> myNode;
    
        public CLHLock() {
            this.tail = new AtomicReference<QNode>(new QNode());
            this.myNode = new ThreadLocal<QNode>() {
                protected QNode initialValue() {
                    return new QNode();
                }
            };
            this.myPred = new ThreadLocal<QNode>();
        }
    
        @Override
        public void lock() {
            // 获取当前线程的代表节点
            QNode node = myNode.get();
            // 将自己的状态设置为true表示获取锁。
            node.locked = true;
            // 将自己放在队列的尾巴,并且返回以前的值。第一次进将获取构造函数中的那个new QNode
            QNode pred = tail.getAndSet(node);
            // 把旧的节点放入前驱节点。
            myPred.set(pred);
            // 判断前驱节点的状态,然后走掉。
            while (pred.locked) {
            }
        }
    
        @Override
        public void unlock() {
            // unlock. 获取自己的node。把自己的locked设置为false。
            QNode node = myNode.get();
            node.locked = false;
            myNode.set(myPred.get());
        }
    }
    
    
    

    简单的看一下CLH的算法定义

    the list, the application thread spin only on a local variable, it constantly polling the precursor state, if it is found that the pre release lock end spin.

    基于list,线程仅在一个局部变量上自旋,它不断轮询前一个节点状态,如果发现前一个节点释放锁结束.

    所以在java中使用了ThreadLocal作为具体实现,AtomicReference为了消除多个线程并发对tail引用Node的影响,核心方法lock()中分为3个步骤去实现

    1. 初始状态 tail指向一个node(head)节点

      private final AtomicReference<Node> tail = new AtomicReference<Node>(new Node());
    2. thread加入等待队列: tail指向新的Node,同时Prev指向tail之前指向的节点,在java代码中使用了getAndSet即CAS操作使用

      Node pred = this.tail.getAndSet(node);
      this.prev.set(pred);
    3. 寻找当前线程对应的node的前驱node然后开始自旋前驱node的status判断是否可以获取lock

      while (pred.locked);

    同理unlock()方法,获取当前线程的node,设置lock status,将当前node指向前驱node(这样操作tail指向的就是前驱node等同于出队操作).至此CLH Lock的过程就结束了

    测试CLHLock

    public class CLHLockDemo2 {
    
        public static void main(String[] args) {
            final Kfc kfc = new Kfc();
            for (int i = 0; i < 10; i++) {
                new Thread("eat" + i) {
                    public void run() {
                        kfc.eat();
                    }
                }.start();
            }
    
        }
    }
    
    class Kfc {
        private final Lock lock = new CLHLock();
        private int i = 0;
    
        public void eat() {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + ": " + --i);
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    
        public void cook() {
            try {
                lock.lock();
                System.out.println(Thread.currentThread().getName() + ": " + ++i);
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    

     运行结果

    eat1: -1
    eat0: -2
    eat3: -3
    eat4: -4
    eat7: -5
    eat2: -6
    eat5: -7
    eat6: -8
    eat8: -9
    eat9: -10
    

    四. CLH优缺点

    CLH队列锁的优点是空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail),CLH的一种变体被应用在了JAVA并发框架中。唯一的缺点是在NUMA系统结构下性能很差,在这种系统结构下,每个线程有自己的内存,如果前趋结点的内存位置比较远,自旋判断前趋结点的locked域,性能将大打折扣,但是在SMP系统结构下该法还是非常有效的一种解决NUMA系统结构的思路是MCS队列锁

    五. 了解与CLH对应的MCS自旋锁

    MCS 自旋锁

    MCS 的名称来自其发明人的名字:John Mellor-Crummey和Michael Scott。
    MCS 的实现是基于链表的,每个申请锁的线程都是链表上的一个节点,这些线程会一直轮询自己的本地变量,来知道它自己是否获得了锁。已经获得了锁的线程在释放锁的时候,负责通知其它线程,这样 CPU 之间缓存的同步操作就减少了很多,仅在线程通知另外一个线程的时候发生,降低了系统总线和内存的开销。实现如下所示:

    import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
    public class MCSLock {
        public static class MCSNode {
            volatile MCSNode next;
            volatile boolean isWaiting = true; // 默认是在等待锁
        }
        volatile MCSNode queue;// 指向最后一个申请锁的MCSNode
        private static final AtomicReferenceFieldUpdater<MCSLock, MCSNode> UPDATER = AtomicReferenceFieldUpdater
                .newUpdater(MCSLock.class, MCSNode.class, "queue");
    
        public void lock(MCSNode currentThread) {
            MCSNode predecessor = UPDATER.getAndSet(this, currentThread);// step 1
            if (predecessor != null) {
                predecessor.next = currentThread;// step 2
                while (currentThread.isWaiting) {// step 3
                }
            } else { // 只有一个线程在使用锁,没有前驱来通知它,所以得自己标记自己已获得锁
                currentThread.isWaiting = false;
            }
        }
    
        public void unlock(MCSNode currentThread) {
            if (currentThread.isWaiting) {// 锁拥有者进行释放锁才有意义
                return;
            }
    
            if (currentThread.next == null) {// 检查是否有人排在自己后面
                if (UPDATER.compareAndSet(this, currentThread, null)) {// step 4
                    // compareAndSet返回true表示确实没有人排在自己后面
                    return;
                } else {
                    // 突然有人排在自己后面了,可能还不知道是谁,下面是等待后续者
                    // 这里之所以要忙等是因为:step 1执行完后,step 2可能还没执行完
                    while (currentThread.next == null) { // step 5
                    }
                }
            }
            currentThread.next.isWaiting = false;
            currentThread.next = null;// for GC
        }
    }
    

    MCS 的能够保证较高的效率,降低不必要的性能消耗,并且它是公平的自旋锁。

    CLH 锁与 MCS 锁的原理大致相同,都是各个线程轮询各自关注的变量,来避免多个线程对同一个变量的轮询,从而从 CPU 缓存一致性的角度上减少了系统的消耗。
    CLH 锁与 MCS 锁最大的不同是,MCS 轮询的是当前队列节点的变量,而 CLH 轮询的是当前节点的前驱节点的变量,来判断前一个线程是否释放了锁。
     

    小结

    CLH Lock是一种比较简单的自旋锁算法之一,因为锁的CAS操作涉及到了硬件的锁定(锁总线或者是锁内存)所以性能和CPU架构也密不可分,该兴趣的同学可以继续深入研究包括MCS锁等。CLH Lock是独占式锁的一种,并且是不可重入的锁,这篇文章是对AQS锁源代码分析的预热篇

    参考内容:

    https://segmentfault.com/a/1190000007094429

    https://blog.csdn.net/faicm/article/details/80501465

    https://blog.csdn.net/aesop_wubo/article/details/7533186

    https://www.jianshu.com/p/0f6d3530d46b

    https://blog.csdn.net/jjavaboy/article/details/78603477

  • 相关阅读:
    结巴分词 0.14 版发布,Python 中文分词库
    Lazarus 1.0.2 发布,Pascal 集成开发环境
    Android全屏 去除标题栏和状态栏
    服务器日志现 Android 4.2 传将添多项新特性
    Percona XtraBackup 2.0.3 发布
    长平狐 Android 强制设置横屏或竖屏 设置全屏
    NetBeans 7.3 Beta 发布,全新的 HTML5 支持
    CppDepend现在已经支持Linux
    GromJS 1.7.18 发布,服务器端的 JavaScript
    Apache OpenWebBeans 1.1.6 发布
  • 原文地址:https://www.cnblogs.com/shoshana-kong/p/10831502.html
Copyright © 2011-2022 走看看