美文网首页
自旋锁学习记录

自旋锁学习记录

作者: 每天都是奥利给 | 来源:发表于2021-04-27 12:58 被阅读0次

参考地址

普通自旋锁

利用AtomicReference.compareAndSet 确定对象的原子性,并通过while不断循环阻塞其他线程。
当上个线程unLock后,阻塞线程跳出while。

public class SpinningLock {
    /**
     * 持有锁的线程 为空标识没有线程持有
     */
    private AtomicReference<Thread> ref = new AtomicReference<>();

    /**
     * 锁
     */
    public void Lock(){
        // 获取当前线程
        Thread currentThread = Thread.currentThread();
        // ref.compareAndSet
        // 1. 当ref =  null时  compareAndSet 把当前线程赋值到ref 并返回true
        // 2. 当ref != null时  compareAndSet 返回false
        while (!ref.compareAndSet(null, currentThread)){
            // 通过循环不断的自旋判断锁是否被其他线程持有 hlod资源
        }
    }

    /**
     * 解锁
     */
    public void unLock(){
        Thread currentThread = Thread.currentThread();
        ref.get();
        ref.compareAndSet(currentThread, null);
    }
}
优点:
  1. 无需上下文切换,速率快
缺点:
  1. conpareAndSet是其核心,底层通过各系统cpu指令实现(依赖硬件)。
  2. 无法保证等待线程按FIFO顺序获得锁(非公平)

自旋锁变种(TicketLock-解决普通自旋锁 公平性问题)

类似排号流程:

lock()

用户A和B去医院排号,A到了1号,B取到了2号 => myNum.set(ticketNum.getAndIncrement());。
医生按照票号顺序叫号对A一顿服务,B就老老实实坐板凳等着 => while (serviceNum.get() != myNum.get())

unlock()

当医生服务完后,看了眼A的票号,下个应该是2号治疗了 => serviceNum.compareAndSet(myNum.get(), myNum.get() + 1);
然后将A请出去 => myNum.remove(); 让还在while等着的B进来。

public class TicketLock implements Lock {
    // 服务号 线程完成作业 +1
    private AtomicInteger serviceNum = new AtomicInteger(0);
    // 取票号 线程进入时取号
    private AtomicInteger ticketNum = new AtomicInteger(0);
    // 当前线程持有号
    private final ThreadLocal<Integer> myNum = new ThreadLocal<>();
    @Override
    public void lock() {
        // 当前线程取号
        myNum.set(ticketNum.getAndIncrement());
        // 当服务号 != 线程所取到的号 死循环阻塞 监听到serviceNum = myNum时退出循环
        while (serviceNum.get() != myNum.get()) {
        }
    }

    @Override
    public void unlock() {
        serviceNum.compareAndSet(myNum.get(), myNum.get() + 1);
        myNum.remove();
    }
}

该变种虽然解决了公平性问题,但是在多处理系统上需要对serviceNum进行读写同步,增大了内存和总线的流量,降低了系统整体性能。

自旋锁变种(CLHLock)

CLHLock发明人是:Craig,Landin and Hagersten 所以才以CLH开头。这是种基于链表,可扩展和高性能的自旋锁。
该设计的思想主要是将线程有序的抽象成一个个Node对象,利用对象的线程共享locked属性,判断是否存在上个节点持有锁,以此阻塞或通过。每次获取锁时将当前node放入尾部链表,将上个node放入前区链表;解锁时获取当前node,置为false,让后续线程通过,再将currNode置为preNode,因为初始化时是个初始对象,相当于平移,这样就将当前node移出节点

public class CLHLock implements Lock {
    // 指向尾部节点
    private final AtomicReference<QNode> tail;
    // 指向前驱节点
    private final ThreadLocal<QNode> preNode;
    // 当前节点
    private final ThreadLocal<QNode> myNode;

    public CLHLock() {
        tail = new AtomicReference<>(new QNode());
        myNode = ThreadLocal.withInitial(QNode::new);
        preNode = new ThreadLocal<>();
    }

    @Override
    public void lock() {
        // 获取一个QNode
        QNode qnode = myNode.get();
        // 设置自己的状态为locked=true表示需要获取锁
        qnode.locked = true;
        // 链表的尾部设置为本线程的qNode,并将之前的尾部设置为当前线程的preNode
        QNode pre = tail.getAndSet(qnode);
        // 把旧的节点放入前驱节点。
        preNode.set(pre);
        // 当前线程在前驱节点的locked字段上旋转,直到前驱节点释放锁资源
        while (pre.locked) {
        }

    }

    @Override
    public void unlock() {
        // 获取当前节点
        QNode qnode = myNode.get();
        // 释放锁操作时将自己的locked设置为false,可以使得自己的后继节点可以结束自旋
        qnode.locked = false;
        // 回收自己这个节点,从虚拟队列中删除
        // 将当前节点引用置为自己的preNode,那么下一个节点的preNode就变为了当前节点的preNode,这样就将当前节点移出了队列
        myNode.set(preNode.get());
    }

    private class QNode {
        // true表示该线程需要获取锁,且不释放锁,为false表示线程释放了锁,且不需要锁 volatile 修饰其它线程可见
        private volatile boolean locked = false;
    }
优点:

空间复杂度低(如果有n个线程,L个锁,每个线程每次只获取一个锁,那么需要的存储空间是O(L+n),n个线程有n个myNode,L个锁有L个tail),CLH的一种变体被应用在了JAVA并发框架中

缺点:

在NUMA系统结构下性能很差(在这种系统结构下,每个线程有自己的内存,如果前趋结点的内存位置比较远,自旋判断前趋结点的locked域,性能将大打折扣)

自旋锁变种(MCSLock)

MCS 来自于其发明人名字的首字母: John Mellor-Crummey和Michael Scott。是一种基于链表的可扩展、高性能、公平的自旋锁。

MCSLock 与 CLHNode的差异

  1. 从代码实现来看,CLH比MCS要简单得多。
  2. 从自旋的条件来看,CLH是在前驱节点的属性上自旋,而MCS是在本地属性变量上自旋。
  • MCSLock:while (!qnode.locked)
  • CLHNode:while (pre.locked)
  1. 从链表队列来看,CLHNode不直接持有前驱节点,CLH锁释放时只需要改变自己的属性;MCSNode直接持有后继节点,MCS锁释放需要改变后继节点的属性。
  2. CLH锁释放时只需要改变自己的属性,MCS锁释放则需要改变后继节点的属性。
  public class MCSLock implements Lock {
    // 尾结点
    private AtomicReference<QNode> tail;
    // 当前节点
    private ThreadLocal<QNode> myNode;

    public MCSLock() {
        tail = new AtomicReference<>(null);
        myNode = ThreadLocal.withInitial(QNode::new);
    }
    @Override
    public void lock() {
        // 获取当前节点
        QNode qnode = myNode.get();
        // 赋值当前节点 并返回旧值(即上个节点)
        QNode preNode = tail.getAndSet(qnode);
        // 上个节点不为空 说明有线程持有资源
        if (preNode != null){
            // 设置当前节点设置为false
            qnode.locked = false;
            // 将上个节点的指针 指向当前节点
            preNode.next = qnode;
            // 等待上个节点执行完毕
            while (!qnode.locked) {
            }
        }
        // 设置当前节点为持有资源进程
        qnode.locked = true;
    }

    @Override
    public void unlock() {
        // 获取当前节点
        QNode qnode = myNode.get();
        if (qnode.next == null) {
            //后面没有等待线程的情况
            if (tail.compareAndSet(qnode, null)) {
                //真的没有等待线程,则直接返回,不需要通知
                return;
            }
            // if (tail.compareAndSet(qnode, null)) return false 说明已经进入了另外一个线程
            while (qnode.next == null) {
            }
        }
        //后面有等待线程,则通知后面的线程
        qnode.next.locked = true;
        qnode.next = null;
    }

    private class QNode {
        /**
         * 是否被qNode所属线程锁定
         */
        private volatile boolean locked = false;
        /**
         * 与CLHLock相比,多了这个真正的next
         */
        private volatile QNode next = null;
    }
}
优点:

申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销,解决NUMA系统结构的思路是MCS队列锁。

相关文章

  • 自旋锁&CLH锁&MCS锁学习记录

    本篇文章主要记录自旋锁、CLH锁、MCS锁的学习所得。关于自旋锁和CLH锁、MCS锁,网上已经有很多内容,而且很类...

  • 自旋锁学习记录

    参考地址[https://www.jianshu.com/p/824b2e4f1eed] 普通自旋锁 利用Atom...

  • iOS 开发中加锁

    [1].OSSpinLock 自旋锁 [1]自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被...

  • 关于自旋锁

    自旋锁是什么? spinlock,不断的自旋(自我循环)尝试获得锁。参考文档:Java中的自旋锁 自旋锁的实现 自...

  • 自旋锁与互斥锁

    自旋锁(Spin lock) 自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保...

  • 线程同步

    一、临界资源 加锁会导致运行时间的增长 二、互斥锁 三、自旋锁 实验发现【自旋锁】的效率较高自旋锁 自旋锁加入休眠...

  • CLH并发队列

    1 什么是自旋锁和互斥锁? 由于CLH锁是一种自旋锁,那么我们先来看看自旋锁是什么? 自旋锁说白了也是一种互斥锁,...

  • 自旋锁

    1. 什么是自旋锁 自旋锁与互斥锁有点类似,但是自旋锁不会引起调用者阻塞,如果自旋锁已经被别的执行单元保持,调用者...

  • iOS中各种锁的性能对比

    自旋锁 与 互斥锁 自旋锁 (spin lock): 如果一个线程需要获取自旋锁,该锁已经被其他线程占用,该线程不...

  • 多线程复习

    自旋锁 & 互斥锁 自旋锁:atomic、OSSpinLock、dispatch_semaphore_t临界区加锁...

网友评论

      本文标题:自旋锁学习记录

      本文链接:https://www.haomeiwen.com/subject/xweurltx.html