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

  • 相关阅读:
    《Think Python》第7章学习笔记
    Arrays和String单元测试
    结对编程项目-四则运算 第二周
    2018-2019-2 20175227张雪莹《Java程序设计》 实验二《Java面向对象程序设计》
    类定义
    20175227张雪莹 2018-2019-2 《Java程序设计》第六周学习总结
    结对编程项目-四则运算 第一周 阶段性总结
    迭代和JDB
    20175227张雪莹 2018-2019-2 《Java程序设计》第五周学习总结
    结对学习小组
  • 原文地址:https://www.cnblogs.com/shoshana-kong/p/10831502.html
Copyright © 2011-2022 走看看